On java8第十三章
函数式编程
函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。
通过修改内存中的代码,使程序可以执行不同的操作,用这种方式来节省代码空间。这种技术被称为自修改代码,只要程序小到几个人就能够维护所有棘手和难懂的汇编代码,你就能让程序运行起来。
使用代码以某种方式操纵其他代码的想法也很有趣,只要能保证它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常吸引人。我们不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。
函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用。
纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。
函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:为“并行编程”,“代码可靠性”和“代码创建和库复用”。
新旧对比
通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,只要能将代码传递给方法,我们就可以控制它的行为。
//创建一个接口
interface Strategy{
//返回值类型String的接口方法
String approach(String msg);
}
//实现Strategy接口
class Soft implements Strategy{
//实现接口方法
@Override
public String approach(String msg) {
return msg.toLowerCase() + "?";
}
}
//创建一个Unrelated类
class Unrelated {
static String twice(String msg) {
return msg + " " + msg;
}
}
public class Strategize {
//创建Strategy引用
Strategy strategy;
//创建String引用
String msg;
//构造器
Strategize(String msg){
strategy=new Soft();// [1] 在构造器中赋值
this.msg=msg;
}
//方法communicate,打印输出Strategize类调用approach()方法后的输出内容
void communicate() {
System.out.println(strategy.approach(msg));
}
//通过方法赋值strategy
void changeStrategy(Strategy strategy) {
this.strategy = strategy;
}
public static void main(String[] args) {
//创建数组
Strategy[] strategies = {
new Strategy() { // [2] 匿名内部类
//元素1
public String approach(String msg) {
return msg.toUpperCase() + "!";
}
},
//元素2,lambad表达式
msg -> msg.substring(0, 5), // [3] Lambda 表达式
//元素3
Unrelated::twice // [4] 方法引用
};
//创建Strategize对象
Strategize s = new Strategize("Hello there");
//调用communicate方法打印输出
s.communicate();
//通过forin循环,执行循环赋值,和打印输出操作
for(Strategy newStrategy : strategies) {
s.changeStrategy(newStrategy); // [5]
s.communicate(); // [6]
}
}
}
Strategy 接口提供了单一的 approach()
方法来承载函数式功能。通过创建不同的 Strategy 对象,我们可以创建不同的行为。
我们一般通过创建一个实现Strategy接口的类来实现这种行为,正如在Soft里所做的。
- [1] 在 Strategize 中,你可以看到 Soft 作为默认策略,在构造函数中赋值。
- [2] 一种较为简洁且更加自然的方法是创建一个匿名内部类。即便如此,仍有相当数量的冗余代码。你总需要仔细观察后才会发现:“哦,我明白了,原来这里使用了匿名内部类。”
-
[3] Java 8 的 Lambda 表达式,其参数和函数体被箭头
->
分隔开。箭头右侧是从 Lambda 返回的表达式。它与单独定义类和采用匿名内部类是等价的,但代码少得多。 -
[4] Java 8 的方法引用,它以
::
为特征。::
的左边是类或对象的名称,::
的右边是方法的名称,但是没有参数列表。 -
[5] 在使用默认的 Soft 策略之后,我们逐步遍历数组中的所有 Strategy,并通过调用
changeStrategy()
方法将每个 Strategy 传入变量s
中。 -
[6] 现在,每次调用
communicate()
都会产生不同的行为,具体取决于此刻正在使用的策略代码对象。我们传递的是行为,而并不仅仅是数据。
Lambda表达式
Lambda 表达式是使用最小可能语法编写的函数定义:
- Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。
- Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。
interface Description {
String brief();
}
interface Body {
String detailed(String head);
}
interface Multi {
String twoArg(String head, Double d);
}
public class LambdaExpressions {
//当只用一个参数,可以不需要括号 ()。 然而,这是一个特例。
static Body bod=h->h+"No Parens!";
//正常情况使用括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。
static Body bod2=(h)->h+"More details";
//如果没有参数,则必须使用括号 () 表示空参数列表。
static Description desc=()->"Short info";
//对于多个参数,将参数列表放在括号 () 中。
static Multi mult=(h,n)->h+n;
//在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return。
static Description moreLines=()->{
System.out.println("moreLines");
return "from moreLines";
};
public static void main(String[] args) {
System.out.println(bod.detailed("oh"));
System.out.println(bod2.detailed("Hi!"));
System.out.println(desc.brief());
System.out.println(mult.twoArg("Pi! ", 3.14159));
System.out.println(moreLines.brief());
}
}
每个接口都有一个单独的方法,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法。
Lambda 表达式的基本语法是:
- 参数。
- 接着
->
,可视为“产出”。 -
->
之后的内容都是方法体。
Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将在本书中尽可能使用它们。
递归
递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。
//创建一个接受 int 型参数并生成 int 的接口
public interface IntCall {
int call(int arg);
}
使用Lambda表达式实现递归操作:
public class RecursiveFibonacci {
IntCall fib;
RecursiveFibonacci(){
//此处使用了lambda表达式赋值
fib=n->n==0?0:
n==1?1:
//递归
fib.call(n-1)+fib.call(n-2);
}
int fibonacci(int n){return fib.call(n);}
public static void main(String[] args) {
RecursiveFibonacci rf=new RecursiveFibonacci();
for (int i = 0; i <=10; i++) {
System.out.println(rf.fibonacci(i));
}
}
}
方法引用
方法引用组成:类名或对象名,后面跟 ::
,然后跟方法名称
interface Callable { // [1]
void call(String s);
}
class Describe {
void show(String msg) { // [2]
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) { // [3]
System.out.println("Hello, " + name);
}
static class Description {
String about;
Description(String desc) { about = desc; }
void help(String msg) { // [4]
System.out.println(about + " " + msg);
}
}
static class Helper {
static void assist(String msg) { // [5]
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
Callable c = d::show; // [6]
c.call("call()"); // [7]
c = MethodReferences::hello; // [8]
c.call("Bob");
c = new Description("valuable")::help; // [9]
c.call("information");
c = Helper::assist; // [10]
c.call("Help!");
}
}
- 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。
-
show()
的签名(参数类型和返回类型)符合 Callable 的call()
的签名。 - hello()
也符合
call()的签名。
- help()
也符合,它是静态内部类中的非静态方法。
- assist()
是静态内部类中的静态方法。
-
我们将 **Describe** 对象的方法引用赋值给 **Callable** ,它没有
show()方法,而是
call()方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 **Callable** 的
call()方法的签名。
-
我们现在可以通过调用
call()来调用
show(),因为 Java 将
call()映射到
show()`。 - 这是一个静态方法引用。
- 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为绑定方法引用。
- 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。
Runnable接口
Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run()
不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable
class Go{
static void go(){
System.out.println("Go::go");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
//传入匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous");
}
}).start();
//使用lambda表达式
new Thread(()-> System.out.println("lambda")){
}.start();
//使用方法引用
new Thread(Go::go).start();
}
}
Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 run()
的方法 start()
。 注意,只有匿名内部类才需要具有名为 run()
的方法。
未绑定的方法引用
未绑定的方法引用是指没有关联对象的普通方法(就是非静态方法),使用未绑定的引用时,我们必须先提供对象:
class X{
String f(){return "X::f()";}
}
interface MakeString{
String make();
}
interface TransformX {
String transform(X x);
}
public class UnboundMethodReference {
public static void main(String[] args) {
TransformX sp=X::f;//个人理解:只是将X对象传递到TransformX函数式接口方法参数列表中
X x = new X();
System.out.println(sp.transform(x)); //[2]
System.out.println(x.f());
}
注意上面代码中,如果被赋值的方法引用和需要赋值的接口方法的签名(参数和返回值)相同,那么对于未绑定的方法引用,必须提供对象实例;**如果需要赋值的接口方法中的参数中有该类的对象,则认为需要一个这样的对象来调用方法,因此参数可以不完全匹配。**不完全匹配的意思是被赋值方法引用除了该对象外,其余参数匹配。
//如果你的方法有更多个参数,就以第一个参数接受this的模式来处理
class This{
void two(int i, double d) {
System.out.println("TWO");
}
void two2(int i, double d) {
System.out.println("TWO2");
}
void three(int i, double d, String s) {
System.out.println("THREE");
}
void four(int i, double d, String s, char c) {
System.out.println("FOUR");
}
}
interface TwoArgs {
void call2(This athis, int i, double d);
}
interface ThreeArgs {
void call3(This athis, int i, double d, String s);
}
interface FourArgs {
void call4(
This athis, int i, double d, String s, char c);
}
public class MultiUnbound {
public static void main(String[] args) {
TwoArgs twoArgs=This::two;//个人理解:将This类绑定two方法,传入twoArgs中
ThreeArgs threeArgs=This::three;
FourArgs fourArgs=This::four;
This athis = new This();
twoArgs.call2(athis,11,3.14);//通过调用接口方法call2时,会使用two方法覆盖call2方法
threeArgs.call3(athis,11,3.14,"Three");
fourArgs.call4(athis, 11, 3.14, "Four", 'Z');
}
}
如果接口中的方法有返回类型,即要求有值进行返回,那么被赋值的方法引用必须要有返回值;如果接口中的方法没有返回类型,那么对于被赋值的方法,其返回值可以有也可以没有。
构造函数引用
可以捕获构造函数的引用,然后通过引用调用该构造函数。
class Dog {
String name;
int age = -1; //对于未知
Dog() { name = "stray"; }
Dog(String nm) { name = nm; }
Dog(String nm, int yrs) { name = nm; age = yrs; }
}
interface MakeNoArgs {
Dog make();
}
interface Make1Arg {
Dog make(String nm);
}
interface Make2Args {
Dog make(String nm, int age);
}
public class CtorReference {
public static void main(String[] args) {
MakeNoArgs mna = Dog::new; // [1]
Make1Arg m1a = Dog::new; // [2]
Make2Args m2a = Dog::new; // [3]
Dog dn = mna.make();
Dog d1 = m1a.make("Comet");
Dog d2 = m2a.make("Ralph", 4);
}
}
Dog 有三个构造函数,函数式接口内的 make()
方法反映了构造函数参数列表( make()
方法名称可以不同)。
对 [1]**,[2]** 和 [3] 中的每一个使用 Dog :: new
。 这三个构造函数只有一个相同名称::: new
,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。
函数式接口
Java 8 引入了 java.util.function
包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。
//此注解强制接口只有一个抽象方法
@FunctionalInterface
interface Functional{
String goodbye(String arg);
}
interface FunctionalNoAnn {
String goodbye(String arg);
//如果添加如下抽象方法,则该接口不是函数式接口
//String goodbye(String arg);
}
public class FunctionalAnnotation {
public String goodbye(String arg){
return "Goodbye, " + arg;
}
public static void main(String[] args) {
FunctionalAnnotation fa=new FunctionalAnnotation();
//使用方法引用方式
Functional f=fa::goodbye;
FunctionalNoAnn fna=fa::goodbye;
// Functional fac = fa; //会报错,因为FunctionalAnnotation 并没有显式地去实现 Functional 接口
//使用lambda表达式
Functional f1=a->"Goodbye"+a;
FunctionalNoAnn fnal=a->"Goodbye"+a;
}
}
在 main()
中把 Functional 和 FunctionalNoAnn 都当作函数式接口。
Functional
和 FunctionalNoAnn
定义接口,然而被赋值的只是方法 goodbye()
。首先,这只是一个方法而不是类;它甚至都不是实现了该接口的类中的方法,将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。
接口的基本命名准则:
- 如果只处理对象而非基本类型,名称则为
Function
,Consumer
,Predicate
等。参数类型通过泛型添加。 - 如果接收的参数是基本类型,则由名称的第一部分表示,如
LongConsumer
,DoubleFunction
,IntPredicate
等,但返回基本类型的Supplier
接口例外。 - 如果返回值为基本类型,则用
To
表示,如ToLongFunction <T>
和IntToLongFunction
。 - 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用
UnaryOperator
,两个参数使用BinaryOperator
。 - 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。
- 如果接收的两个参数类型不同,则名称中有一个
Bi
。
多参数函数式接口
java.util.functional
中的接口是有限的。可以自行创建
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
//测试
public class TriFunctionTest {
static int f(int i, long l, double d) { return 99; }
public static void main(String[] args) {
TriFunction<Integer, Long, Double, Integer> tf =
TriFunctionTest::f;
tf = (i, l, d) -> 12;
}
}
同时测试了方法引用和 Lambda 表达式。
缺少基本类型的函数
使用了包装类型,装箱和拆箱负责它与基本类型之间的来回转换。
高阶函数
高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数。什么样的参数是函数类型的参数?要看该参数是否是一个函数式接口,函数式接口只会有一个方法,会使用 @FunctionalInterface 这个注解来修饰。
//产生一个函数
//继承一个接口
interface FuncSS extends Function<String, String> {} // [1]
public class ProduceFunction {
//produce就是高阶函数
static FuncSS produce() {
//使用lambda在方法中创建和返回一个函数
return s -> s.toLowerCase(); // [2]
}
public static void main(String[] args) {
FuncSS f = produce();
System.out.println(f.apply("YELLING"));
}
}
[1] 使用继承,可以轻松地为专用接口创建别名。
[2] 使用 Lambda 表达式,可以轻松地在方法中创建和返回一个函数。
//消费一个函数,消费函数需要在参数列表正确地描述函数类型。
class One{}
class Two{}
public class ConsumeFunction {
static Two consume(Function<One,Two> onetwo){
return onetwo.apply(new One());
}
public static void main(String[] args) {
Two two=consume(one->new Two());
}
}
当基于消费函数生成新函数时,事情就变得相当有趣了。
class I {
@Override
public String toString() { return "I"; }
}
class O {
@Override
public String toString() { return "O"; }
}
public class TransformFunction {
static Function<I,O> transform(Function<I,O> in){
return in.andThen(o -> {
System.out.println(o);
return o;
});
}
public static void main(String[] args) {
Function<I,O> f2=transform(i -> {
System.out.println(i);
return new O();
});
// Function<I,O> f2=transform(new Function<I, O>() {
// @Override
// public O apply(I i) {
// System.out.println(i);
// return new O();
// }
// });
O o=f2.apply(new I());
}
}
transform()
生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。
这里使用到了 Function
接口中名为 andThen()
的默认方法,该方法专门用于操作函数。 顾名思义,在调用 in
函数之后调用 andThen()
(还有个 compose()
方法,它在 in
函数之前应用新函数)。 要附加一个 andThen()
函数,我们只需将该函数作为参数传递。 transform()
产生的是一个新函数,它将 in
的动作与 andThen()
参数的动作结合起来。
闭包
闭包包含*(未绑定到特定对象)变量;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。
就是可以绑定变量作用域,那么很显然,JAVA是完全支持闭包的,
public class Closure1 {
int i=0;
IntSupplier makeFun(int x){
//此处调用全局变量i
return ()->x + i++;
}
}
结论:
- JAVA支持变量作用域,完全支持闭包;
- 并不是只有函数式编程语言才支持闭包,见代码片段1,完全是面向对象的写法;
- 相比与Javascript等函数式编程语言,由于final的限定,JAVA并不支持对变量作用域值的修改;
- 由于JAVA严格的面向对象设计语言,对于函数的调用必须加上对象名限定(如类名、实例名及接口名称);
- 闭包没有那么神奇,也没有那么高不可攀;
//使用List,尝试在作用域实现添加元素
public class Closure8 {
Supplier<List<Integer>> makeFun() {
final List<Integer> ai = new ArrayList<>();
ai.add(1);
return () -> ai;
}
public static void main(String[] args) {
Closure8 c7 = new Closure8();
List<Integer>
l1 = c7.makeFun().get(),
l2 = c7.makeFun().get();
System.out.println(l1);
System.out.println(l2);
l1.add(42);
l2.add(96);
System.out.println(l1);
System.out.println(l2);
}
}
/*
结果:
[1]
[1]
[1, 42]
[1, 96]
*/
改变了 List 的内容却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 makeFun()
时,其实都会创建并返回一个全新而非共享的 ArrayList
。也就是说,每个闭包都有自己独立的 ArrayList
,它们之间互不干扰。
作为闭包的内部类
实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)。在 Java 8 之前,变量 x
和 i
必须被明确声明为 final
。在 Java 8 中,内部类的规则放宽,包括等同 final 效果。
public class AnonymousClosure {
IntSupplier makeFun(int x) {
int i = 0;
// 同样规则的应用:
// i++; // 非等同 final 效果
// x++; // 同上
return new IntSupplier() {
public int getAsInt() { return x + i; }
};
}
}
函数组合
函数组合(Function Composition)意为“多个函数组合成新函数”。它通常是函数式编程的基本组成部分。
组合方法 | 支持接口 |
---|---|
andThen(argument) 根据参数执行原始操作 |
Function BiFunction Consumer BiConsumer IntConsumer LongConsumer DoubleConsumer UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator |
compose(argument) 根据参数执行原始操作 |
Function UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
and(argument) 短路逻辑与原始谓词和参数谓词 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
or(argument) 短路逻辑或原始谓词和参数谓词 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
negate() 该谓词的逻辑否谓词 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
//实例
public class FunctionComposition {
static Function<String,String> f1=s -> {
System.out.println(s);
return s.replace('A','_');
},
f2=s->s.substring(3),
f3=s->s.toLowerCase(),
f4=f1.compose(f2).andThen(f3);
public static void main(String[] args) {
System.out.println(f4.apply("GO AFTER ALL AMBULANCES"));
}
}
当 f1
获得字符串时,它已经被f2
剥离了前三个字符。这是因为 compose(f2)
表示 f2
的调用发生在 f1
之前。
//更复杂的组合
public class PredicateComposition {
static Predicate<String>
p1=s->s.contains("bar"),
p2=s->s.length()<5,
p3=s->s.contains("foo"),
//解释:如果字符串中不包含 bar 且长度小于 5,或者它包含 foo ,则结果为 true。
p4=p1.negate().and(p2).or(p3);
public static void main(String[] args) {
//返回顺序排列流,其元素为指定的值
Stream.of("bar", "foobar", "foobaz", "fongopuckey")
.filter(p4)//返回由该流匹配给定谓词的元素的流
.forEach(System.out::println);
}
}
首先,我创建了一个字符串对象的流,然后将每个对象传递给 filter()
操作。 filter()
使用 p4
的谓词来确定对象的去留。最后我们使用 forEach()
将 println
方法引用应用在每个留存的对象上。
从输出结果我们可以看到 p4
的工作流程:任何带有 "foo"
的字符串都得以保留,即使它的长度大于 5。 "fongopuckey"
因长度超出且不包含 foo
而被丢弃。
柯里化和部分求值
柯里化意为:将一个多参数的函数,转换为一系列单参数函数。
public class CurryingAndPartials {
//未柯里化:
static String uncurried(String a,String b){
return a+b;
}
public static void main(String[] args) {
//柯里化函数:
Function<String,Function<String,String>> sum=
//这一连串的箭头很巧妙。注意,在函数接口声明中,第二个参数是另一个函数
a->b->a+b;
//调用未柯里化方法
System.out.println(uncurried("Hi ", "Ho"));
//柯里化的目的是能够通过提供一个参数来创建一个新函数,所以现在有了一个“带参函数”和剩下的 “*函数”(free argumnet)
// 实际上,你从一个双参数函数开始,最后得到一个单参数函数。
Function<String,String> sumHi=sum.apply("Hup");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
通过添加级别来柯里化一个三参数函数
public class Curry3Args {
public static void main(String[] args) {
//三层级别
Function<String,
Function<String,
Function<String, String>>> sum =
a -> b -> c -> a + b + c;
//两层级别
Function<String,
Function<String, String>> hi =
sum.apply("Hi ");
//单层级别
Function<String, String> ho =
hi.apply("Ho ");
System.out.println(ho.apply("Hup"));
}
}
对于每个级别的箭头级联(Arrow-cascading),你都要在类型声明中包裹另一层 Function。
注意:处理基本类型和装箱时,请使用适当的函数式接口
纯函数式编程
即使没有函数式支持,像 C 这样的基础语言,也可以按照一定的原则编写纯函数式程序。Java 8 让函数式编程更简单,不过我们要确保一切是 final
的,同时你的所有方法和函数没有副作用。因为 Java 在本质上并非是不可变语言,所以编译器对我们犯的错误将无能为力。
这种情况下,我们可以借助第三方工具[^9],但使用 Scala 或 Clojure 这样的语言可能更简单。因为它们从一开始就是为保持不变性而设计的。你可以采用这些语言来编写你的 Java 项目的一部分。如果必须要用纯函数式编写,则可以用 Scala(需要遵循一些规则) 或 Clojure (遵循的规则更少)。虽然 Java 支持并发编程,但如果这是你项目的核心部分,你应该考虑在项目部分功能中使用 Scala
或 Clojure
之类的语言。