【注】本文译自: Introduction To Pragmatic Functional Java - DZone Java

实用函数式(Pragmatic Funcational) Java 是一种基于函数式编程概念的现代、非常简洁但可读的 Java 编码风格。

实用函数式 Java (PFJ) 试图定义一种新的惯用 Java 编码风格。编码风格,将完全利用当前和即将推出的 Java 版本的所有功能,并涉及编译器来帮助编写简洁但可靠和可读的代码。
虽然这种风格甚至可以在 Java 8 中使用,但在 Java 11 中它看起来更加简洁和简洁。它在 Java 17 中变得更具表现力,并受益于每个新的 Java 语言功能。
但 PFJ 不是免费的午餐,它需要开发人员的习惯和方法发生重大改变。改变习惯并不容易,传统的命令式习惯尤其难以解决。
这值得么? 确实! PFJ 代码简洁、富有表现力且可靠。它易于阅读和维护,并且在大多数情况下,如果代码可以编译 - 它可以工作!

实用函数式 Java 的元素

PFJ 源自一本精彩的 Effective Java 书籍,其中包含一些额外的概念和约定,特别是源自函数式编程(FP:Functional Programming)。请注意,尽管使用了 FP 概念,但 PFJ 并未尝试强制执行特定于 FP 的术语。(尽管对于那些有兴趣进一步探索这些概念的人,我们也提供了参考)。
PFJ专注于:

  • 减轻心理负担。
  • 提高代码可靠性。
  • 提高长期可维护性。
  • 借助编译器来帮助编写正确的代码。
  • 让编写正确的代码变得简单而自然,编写不正确的代码虽然仍然可能,但应该需要付出努力。

尽管目标雄心勃勃,但只有两个关键的 PFJ 规则:

  • 尽可能避免 null
  • 没有业务异常。

下面,更详细地探讨了每个关键规则:

尽可能避免 null(ANAMAP 规则)

变量的可空性是特殊状态之一。它们是众所周知的运行时错误和样板代码的来源。为了消除这些问题并表示可能丢失的值,PFJ 使用 Option<T> 容器。这涵盖了可能出现此类值的所有情况 - 返回值、输入参数或字段。
在某些情况下,例如出于性能或与现有框架兼容性的原因,类可能会在内部使用 null。这些情况必须清楚记录并且对类用户不可见,即所有类 API 都应使用 Option<T>
这种方法有几个优点:

  • 可空变量在代码中立即可见。无需阅读文档、检查源代码或依赖注释。
  • 编译器区分可为空和不可为空的变量,并防止它们之间的错误赋值。
  • 消除了 null 检查所需的所有样板。

无业务异常(NBE 规则)

PFJ 仅使用异常来表示致命的、不可恢复的(技术)故障的情况。此类异常可能仅出于记录和/或正常关闭应用程序的目的而被拦截。不鼓励并尽可能避免所有其他异常及其拦截。
业务异常是特殊状态的另一种情况。为了传播和处理业务级错误,PFJ 使用 Result<T> 容器。同样,这涵盖了可能出现错误的所有情况 - 返回值、输入参数或字段。实践表明,字段很少(如果有的话)需要使用这个容器。
没有任何正当的情况可以使用业务级异常。与通过专用包装方法与现有 Java 库和遗留代码交互。Result<T> 容器包含这些包装方法的实现。
无业务异常规则具有以下优点:

  • 可以返回错误的方法在代码中立即可见。 无需阅读 文档、检查源代码或分析调用树,以检查可以抛出哪些异常以及在哪些条件下被抛出。
  • 编译器强制执行正确的错误处理和传播。
  • 几乎没有错误处理和传播的样板。
  • 我们可以为快乐的日子场景编写代码,并在最方便的点处理错误 - 异常的原始意图,这一点实际上从未实现过。
  • 代码保持可组合、易于阅读和推理,在执行流程中没有隐藏的中断或意外的转换——你读到的就是将要执行的

将遗留代码转换为 PFJ 风格的代码

好的,关键规则看起来不错而且很有用,但是真正的代码会是什么样子呢?
让我们从一个非常典型的后端代码开始:

public interface UserRepository {User findById(User.Id userId);
}public interface UserProfileRepository {UserProfile findById(User.Id userId);
}public class UserService {private final UserRepository userRepository;private final UserProfileRepository userProfileRepository;public UserWithProfile getUserWithProfile(User.Id userId) {User user = userRepository.findById(userId);if (user == null) {throw UserNotFoundException("User with ID " + userId + " not found");}UserProfile details = userProfileRepository.findById(userId);return UserWithProfile.of(user, details == null ? UserProfile.defaultDetails() : details);}
}

示例开头的接口是为了上下文清晰而提供的。主要的兴趣点是 getUserWithProfile 方法。我们一步一步来分析。

  • 第一条语句从用户存储库中检索 user 变量。
  • 由于用户可能不存在于存储库中,因此 user 变量可能为 null。以下 null 检查验证是否是这种情况,如果是,则抛出业务异常。
  • 下一步是检索用户配置文件详细信息。缺乏细节不被视为错误。相反,当缺少详细信息时,配置文件将使用默认值。

上面的代码有几个问题。首先,如果存储库中不存在值,则返回 null 从接口看并不明显。 我们需要检查文档,研究实现或猜测这些存储库是如何工作的。
有时使用注解来提供提示,但这仍然不能保证 API 的行为。
为了解决这个问题,让我们将规则应用于存储库:

public interface UserRepository {Option<User> findById(User.Id userId);
}public interface UserProfileRepository {Option<UserProfile> findById(User.Id userId);
}

现在无需进行任何猜测 - API 明确告知可能不存在返回值。
现在让我们再看看 getUserWithProfile 方法。 要注意的第二件事是该方法可能会返回一个值或可能会引发异常。这是一个业务异常,因此我们可以应用该规则。更改的主要目标 - 明确方法可能返回值错误的事实:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {

好的,现在我们已经清理了 API,可以开始更改代码了。第一个变化是由 userRepository 现在返回
Option<User> 引起的:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<User> user = userRepository.findById(userId);
}

现在我们需要检查用户是否存在,如果不存在,则返回一个错误。使用传统的命令式方法,代码应该是这样的:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<User> user = userRepository.findById(userId);if (user.isEmpty()) {return Result.failure(Causes.cause("User with ID " + userId + " not found"));}

}
代码看起来不是很吸引人,但也不比原来的差,所以暂时保持原样。
下一步是尝试转换剩余部分的代码:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<User> user = userRepository.findById(userId);if (user.isEmpty()) {return Result.failure(Causes.cause("User with ID " + userId + " not found"));}Option<UserProfile> details = userProfileRepository.findById(userId);}

问题来了:详细信息和用户存储在 Option<T> 容器中,因此要组装 UserWithProfile,我们需要以某种方式提取值。这里可能有不同的方法,例如,使用 Option.fold() 方法。生成的代码肯定不会很漂亮,而且很可能会违反规则。
还有另一种方法 - 使用 Option<T> 是具有特殊属性的容器这一事实。
特别是,可以使用 Option.map()Option.flatMap() 方法转换 Option<T> 中的值。此外,我们知道,details 值将由存储库提供或替换为默认值。为此,我们可以使用 Option.or() 方法从容器中提取详细信息。让我们试试这些方法:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<User> user = userRepository.findById(userId);if (user.isEmpty()) {return Result.failure(Causes.cause("User with ID " + userId + " not found"));}UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());Option<UserWithProfile> userWithProfile =  user.map(userValue -> UserWithProfile.of(userValue, details));}

现在我们需要编写最后一步 - 将 userWithProfile 容器从 Option<T> 转换为 Result<T>

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<User> user = userRepository.findById(userId);if (user.isEmpty()) {return Result.failure(Causes.cause("User with ID " + userId + " not found"));}UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());Option<UserWithProfile> userWithProfile =  user.map(userValue -> UserWithProfile.of(userValue, details));return userWithProfile.toResult(Cause.cause(""));
}

我们暂时将 return 语句中的错误原因留空,然后再次查看代码。
我们可以很容易地发现一个问题:我们肯定知道 userWithProfile 总是存在 - 当 user 不存在时,上面已经处理了这种情况。我们怎样才能解决这个问题?
请注意,我们可以在不检查用户是否存在的情况下调用 user.map()。仅当 user 存在时才会应用转换,否则将被忽略。 这样,我们可以消除 if(user.isEmpty())检查。让我们在传递给 user.map() 的 lambda 中移动对 Userdetails 检索和转换到 UserWithProfile 中:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<UserWithProfile> userWithProfile = userRepository.findById(userId).map(userValue -> {UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());return UserWithProfile.of(userValue, details);});return userWithProfile.toResult(Cause.cause(""));
}

现在需要更改最后一行,因为 userWithProfile 可能会缺失。该错误将与以前的版本相同,因为仅当 userRepository.findById(userId) 返回的值缺失时,userWithProfile 才会缺失:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {Option<UserWithProfile> userWithProfile = userRepository.findById(userId).map(userValue -> {UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());return UserWithProfile.of(userValue, details);});return userWithProfile.toResult(Causes.cause("User with ID " + userId + " not found"));
}

最后,我们可以内联 detailsuserWithProfile,因为它们仅在创建后立即使用一次:

public Result<UserWithProfile> getUserWithProfile(User.Id userId) {return userRepository.findById(userId).map(userValue -> UserWithProfile.of(userValue, userProfileRepository.findById(userId).or(UserProfile.defaultDetails()))).toResult(Causes.cause("User with ID " + userId + " not found"));
}

请注意缩进如何帮助将代码分组为逻辑链接的部分。
让我们来分析结果代码:

  • 代码更简洁,为快乐的日子场景编写,没有明确的错误或 null 检查,没有干扰业务逻辑
  • 没有简单的方法可以跳过或避免错误或 null 检查,编写正确可靠的代码是直接而自然的。

不太明显的观察:

  • 所有类型都是自动派生的。这简化了重构并消除了不必要的混乱。如果需要,仍然可以添加类型。
  • 如果在某个时候存储库将开始返回 Result<T> 而不是 Option<T>,代码将保持不变,除了最后一个转换 (toResult) 将被删除。
  • 除了用 Option.or() 方法替换三元运算符之外,结果代码看起来很像如果我们将传递给 lambda 内部的原始 return 语句中的代码移动到 map() 方法。

最后一个观察对于开始方便地编写(阅读通常不是问题)PFJ 风格的代码非常有用。它可以改写为以下经验规则:在右侧寻找值。比较一下:

User user = userRepository.findById(userId); // <-- 值在表达式左边

return userRepository.findById(userId)
.map(user -> ...); // <-- 值在表达式右边

这种有用的观察有助于从遗留命令式代码风格向 PFJ 转换。

与遗留代码交互

不用说,现有代码不遵循 PFJ 方法。它抛出异常,返回 null 等等。有时可以重新编写此代码以使其与 PFJ 兼容,但通常情况并非如此。对于外部库和框架尤其如此。

调用遗留代码

遗留代码调用有两个主要问题。它们中的每一个都与违反相应的 PFJ 规则有关:

处理业务异常

Result<T> 包含一个名为 lift() 的辅助方法,它涵盖了大多数用例。方法签名看起来是这样:

static <R> Result<R> lift(FN1<? extends Cause, ? super Throwable> exceptionMapper, ThrowingSupplier<R> supplier)

第一个参数是将异常转换为 Cause 实例的函数(反过来,它用于在失败情况下创建 Result<T> 实例)。第二个参数是 lambda,它封装了对需要与 PFJ 兼容的实际代码的调用。
Causesutility 类中提供了最简单的函数,它将异常转换为 Cause 的实例:fromThrowable()。它们可以与 Result.lift() 一起使用,如下所示:

public static Result<URI> createURI(String uri) {return Result.lift(Causes::fromThrowable, () -> URI.create(uri));
}

处理 null 值返回

这种情况相当简单 - 如果 API 可以返回 null,只需使用 Option.option() 方法将其包装到 Option<T> 中。

提供遗留 API

有时需要允许遗留代码调用以 PFJ 风格编写的代码。特别是,当一些较小的子系统转换为 PFJ 风格时,通常会发生这种情况,但系统的其余部分仍然以旧风格编写,并且需要保留 API。最方便的方法是将实现拆分为两部分——PFJ 风格的 API 和适配器,它只将新 API 适配到旧 API。这可能是一个非常有用的简单辅助方法,如下所示:

public static <T> T unwrap(Result<T> value) {return value.fold(cause -> { throw new IllegalStateException(cause.message()); },content -> content);
}

Result<T> 中没有提供随时可用的辅助方法,原因如下:

  • 可能有不同的用例,并且可以抛出不同类型的异常(已检查和未检查)。
  • Cause 转换为不同的特定异常在很大程度上取决于特定的用例。

管理变量作用域

本节将专门介绍在编写 PFJ 风格代码时出现的各种实际案例。
下面的示例假设使用 Result<T>,但这在很大程度上无关紧要,因为所有考虑因素也适用于 Option<T>。此外,示例假定示例中调用的函数被转换为返回 Result<T> 而不是抛出异常。

嵌套作用域

函数风格代码大量使用 lambda 来执行 Option<T>Result<T> 容器内的值的计算和转换。每个 lambda 都隐式地为其参数创建了作用域——它们可以在 lambda 主体内部访问,但不能在其外部访问。
这通常是一个有用的属性,但对于传统的命令式代码,它很不寻常,一开始可能会觉得不方便。幸运的是,有一种简单的技术可以克服感知上的不便。
我们来看看下面的命令式代码:

var value1 = function1(...); // function1()可能抛出异常
var value2 = function2(value1, ...); // function2() 可能抛出异常
var value3 = function3(value1, value2, ...); // function3() 可能抛出异常

变量 value1 应该可访问以调用 function2() 和 function3()。 这确实意味着直接转换为 PFJ 样式将不起作用:

function1(...)
.flatMap(value1 -> function2(value1, ...))
.flatMap(value2 -> function3(value1, value2, ...)); // <-- 错, value1 不可访问

为了保持值的可访问性,我们需要使用嵌套作用域,即嵌套调用如下:

function1(...)
.flatMap(value1 -> function2(value1, ...).flatMap(value2 -> function3(value1, value2, ...)));

第二次调用 flatMap() 是针对 function2 返回的值而不是第一个 flatMap() 返回的值。通过这种方式,我们将 value1 保持在范围内,并使 function3 可以访问它。
尽管可以创建任意深度的嵌套作用域,但通常多个嵌套作用域更难阅读和遵循。在这种情况下,强烈建议将更深的范围提取到专用函数中。

平行作用域

另一个经常观察到的情况是需要计算/检索几个独立的值,然后进行调用或构建一个对象。让我们看看下面的例子:

var value1 = function1(...);    // function1() 可能抛出异常
var value2 = function2(...);    // function2() 可能抛出异常
var value3 = function3(...);    // function3() 可能抛出异常
return new MyObject(value1, value2, value3);

乍一看,转换为 PFJ 样式可以与嵌套作用域完全相同。每个值的可见性将与命令式代码相同。不幸的是,这会使范围嵌套很深,尤其是在需要获取许多值的情况下。
对于这种情况,Option<T>Result<T> 提供了一组 all() 方法。这些方法执行所有值的“并行”计算并返回 MapperX<...> 接口的专用版本。 这个接口只有三个方法—— id()map()flatMap()map()flatMap() 方法的工作方式与 Option<T>Result<T> 中的相应方法完全相同,只是它们接受具有不同数量参数的 lambda。让我们来看看它在实践中是如何工作的,并将上面的命令式代码转换为 PFJ 样式:

return Result.all(function1(...),function2(...),function3(...)).map(MyObject::new);

除了紧凑和扁平之外,这种方法还有一些优点。首先,它明确表达意图——在使用前计算所有值。命令式代码按顺序执行此操作,隐藏了原始意图。第二个优点 - 每个值的计算是独立的,不会将不必要的值带入范围。这减少了理解和推理每个函数调用所需的上下文。

替代作用域

一个不太常见但仍然很重要的情况是我们需要检索一个值,但如果它不可用,那么我们使用该值的替代来源。当有多个替代方案可用时,这种情况的频率甚至更低,而且在涉及错误处理时会更加痛苦。
我们来看看下面的命令式代码:

MyType value;try {value = function1(...);
} catch (MyException e1) {try {value = function2(...);    } catch(MyException e2) {try {value = function3(...);} catch(MyException e3) {... // repeat as many times as there are alternatives}}
}

代码是人为设计的,因为嵌套案例通常隐藏在其他方法中。尽管如此,整体逻辑并不简单,主要是因为除了选择值之外,我们还需要处理错误。错误处理使代码变得混乱,并使初始意图 - 选择第一个可用的替代方案 - 隐藏在错误处理中。
转变为 PFJ 风格使意图非常清晰:

var value = Result.any(function1(...),function2(...),function3(...));

不幸的是,这里有一个重要的区别:原始命令式代码仅在必要时计算第二个和后续替代项。在某些情况下,这不是问题,但在许多情况下,这是非常不可取的。幸运的是,Result.any() 有一个惰性版本。使用它,我们可以重写代码如下:

var value = Result.any(function1(...),() -> function2(...),() -> function3(...));

现在,转换后的代码的行为与它的命令式对应代码完全一样。

Option<T> 和 Result<T> 的简要技术概述

这两个容器在函数式编程术语中是单子(monad)。
Option<T>Option/Optional/Maybe monad 的直接实现。
Result<T>Either<L,R> 的特意简化和专门版本:左类型是固定的,应该实现 Cause 接口。专业化使 API 与 Option<T> 非常相似,并以失去通用性为代价消除了许多不必要的输入。
这个特定的实现集中在两件事上:

  • 与现有 JDK 类(如 Optional<T>Stream<T>)之间的互操作性
  • 用于明确意图表达的 API

最后一句话值得更深入的解释。
每个容器都有几个核心方法:

  • 工厂方法
  • map() 转换方法,转换值但不改变特殊状态:present Option<T> 保持 present,success Result<T> 保持 success
  • flatMap() 转换方法,除了转换之外,还可以改变特殊状态:将 Option<T> present 转换为 empty 或将 Result<T> success 转换为 failure
  • fold() 方法,它同时处理两种情况(Option<T>present/emptyResult<T>success/failure)。

除了核心方法,还有一堆辅助方法,它们在经常观察到的用例中很有用。
在这些方法中,有一组方法是明确设计来产生副作用的。
Option<T>有以下副作用的方法:

Option<T> whenPresent(Consumer<? super T> consumer);
Option<T> whenEmpty(Runnable action);
Option<T> apply(Runnable emptyValConsumer, Consumer<? super T> nonEmptyValConsumer);

Result<T> 有以下副作用的方法:

Result<T> onSuccess(Consumer<T> consumer);
Result<T> onSuccessDo(Runnable action);
Result<T> onFailure(Consumer<? super Cause> consumer);
Result<T> onFailureDo(Runnable action);
Result<T> apply(Consumer<? super Cause> failureConsumer, Consumer<? super T> successConsumer);

这些方法向读者提供了代码处理副作用而不是转换的提示。

其他有用的工具

除了 Option<T>Result<T> 之外,PFJ 还使用了一些其他通用类。下面,将对每种方法进行更详细地描述。

Functions(函数)

JDK 提供了许多有用的功能接口。不幸的是,通用函数的函数式接口仅限于两个版本:单参数 Function<T, R> 和两个参数 BiFunction<T, U, R>
显然,这在许多实际情况中是不够的。此外,出于某种原因,这些函数的类型参数与 Java 中函数的声明方式相反:结果类型列在最后,而在函数声明中,它首先定义。
PFJ 为具有 1 到 9 个参数的函数使用一组一致的函数接口。 为简洁起见,它们被称为 FN1…FN9。到目前为止,还没有更多参数的函数用例(通常这是代码异味)。但如果有必要,该清单可以进一步扩展。

Tuples(元组)

元组是一种特殊的容器,可用于在单个变量中存储多个不同类型的值。与类或记录不同,存储在其中的值没有名称。这使它们成为在保留类型的同时捕获任意值集的不可或缺的工具。这个用例的一个很好的例子是 Result.all()Option.all() 方法集的实现。
在某种意义上,元组可以被认为是为函数调用准备的一组冻结的参数。从这个角度来看,让元组内部值只能通过 map() 方法访问的决定听起来很合理。然而,具有 2 个参数的元组具有额外的访问器,可以使用 Tuple2<T1,T2> 作为各种 Pair<T1,T2> 实现的替代。
PFJ 使用一组一致的元组实现,具有 0 到 9 个值。提供具有 0 和 1 值的元组以保持一致性。

结论

实用函数式 Java 是一种基于函数式编程概念的现代、非常简洁但可读的 Java 编码风格。与传统的惯用 Java 编码风格相比,它提供了许多好处:

  • PFJ 借助 Java 编译器来帮助编写可靠的代码:
  • 编译的代码通常是有效的
  • 许多错误从运行时转移到编译时
  • 某些类别的错误,例如 NullPointerException 或未处理的异常,实际上已被消除
  • PFJ 显着减少了与错误传播和处理以及 null 检查相关的样板代码量
  • PFJ 专注于清晰表达意图并减少心理负担

实用函数式 Java (PFJ)简介相关推荐

  1. 夯实Java基础系列15:Java注解简介和最佳实践

    Java注解简介 注解如同标签 Java 注解概述 什么是注解? 注解的用处 注解的原理 元注解 JDK里的注解 注解处理器实战 不同类型的注解 类注解 方法注解 参数注解 变量注解 Java注解相关 ...

  2. Java EE 简介

    Java EE 简介 Java EE 基本架构 JavaEE 的诞生是为了解决传统 C/S 架构的弊端:客户端臃肿庞大,扩展性差等弊端. JavaEE 将传统的两层结构细分为了四层. 这四层分别是:C ...

  3. java程序包不存在_第一章 Java语言简介

    Java语言简介 Java之父 James Gosling(詹姆斯·高斯林) 正式诞生时间 1995年,已有二十多年历史 三大方向 JavaSE(桌面版,基础需重点掌握) JavaME(移动版,现在基 ...

  4. (一:NIO系列)JAVA NIO 简介

    出处:JAVA NIO 简介 Java 中 New I/O类库 是由 Java 1.4 引进的异步 IO.由于之前老的I/O类库是阻塞I/O,New I/O类库的目标就是要让Java支持非阻塞I/O, ...

  5. java轻量级并行工具类_16 个超级实用的 Java 工具类

    原标题:16 个超级实用的 Java 工具类 源 /juejin 在Java中,工具类定义了一组公共方法,这篇文章将介绍Java中使用最频繁及最通用的Java工具类.以下工具类.方法按使用流行度排名, ...

  6. ibm java_IBM i 上Java 虚拟机简介

    Abstract IBM i 上Java 虚拟机简介 Body Java在IT业界有多流行?google上搜索一下java, 结果一目了然(今天早上我搜索到的记录数是840000000条).Java之 ...

  7. Java异常(一) Java异常简介及其架构

    概要 本章对Java中的异常进行介绍.内容包括: Java异常简介 Java异常框架 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3544168.htm ...

  8. Java:计算机编程语言Java的简介、编程环境/工具、如何学习Java之详细攻略

    Java:计算机编程语言Java的简介.编程环境/工具.如何学习Java之详细攻略 目录 Java的简介 1.Java的工作原理--基于Eclipse等编程Java语言→定义Java类→Java虚拟机 ...

  9. OpenCV Java开发简介

    OpenCV Java开发简介 OpenCV Java开发简介 我们将在本指南中做什么 获得适当的OpenCV 下载 构建 Java示例与Ant Java和Scala的SBT项目 运行SBT样本 Op ...

最新文章

  1. 下载keep运动软件_我把私教带回了家,Keep智能动感单车体验|钛极客
  2. tableau实战系列(十八)-通过可视化实现购物篮关联分析( Market Basket Analysis),关联物品之间的关联关系
  3. 细细品味C#——文件操作
  4. 【并查集】打击犯罪(ssl 2342)
  5. 什么是javax.ws.rs.core.context? [第3部分]
  6. java宋江,Java编程内功-数据结构与算法「单链表」,
  7. BBC纪录片任正非谈创业:华为是谁?
  8. mysql乱码utfmb4_MySQL乱码问题以及utf8mb4字符集
  9. linux系列(一):ls命令
  10. vue 根据接口数据筛选_如何根据行值变化筛选数据
  11. 11. 禁止异常(exceptions)流出destructors之外
  12. 微软宣布以197亿美元现金收购语音识别巨头Nuance
  13. 衡量连通图连通性一些指标(r-reachable, r-robust)
  14. paddle.paramattr转换为torch框架下算法
  15. 随手记录第二话 -- 高并发情况下秒杀、抢红包都有哪些实现方式?
  16. 计算机程序由算法,计算机程序算法试题.pdf
  17. 同程旅游网开放平台SDK开发完成
  18. Conv2Former
  19. 为何说婚姻不是女人的全部--蝉思读书社给我的诸多感悟
  20. [转]一个清华计算机博士生的退学申请

热门文章

  1. 公众号内打开提示404_【软件搜索】公众号内软件搜索
  2. centos7 安装psutil
  3. 51单片机STC89C52RC进阶 – 在面包板上构建51单片机最小系统
  4. 微信分销系统,你使用了吗?
  5. 论文阅读笔记-Glyce: Glyph-vectors for Chinese CharacterRepresentations
  6. [翻译] EF Core 概述
  7. Java Spring Data Redis实战与配置参数详解 application.properties
  8. 仿淘宝右侧tab栏切换
  9. mysql5.7 最佳实践_MySQL 5.7安装(多实例)最佳实践
  10. laravel小札之Gate与Policy