减少 Java 8 的学习指南
简介
reduce()
方法是Java 8对Stream API中的折叠实现需求的回应。
折叠是一个非常有用和常见的函数式编程特性。它对一个元素的集合进行操作,使用某种操作返回一个单一的结果。
注意: 折叠也被称为减少、聚集、累积和压缩,这些术语都适用于同一个概念。
也就是说--它是最具延展性、灵活性和适用性的操作之一--它非常常用于计算集合的聚合结果,并在分析和数据驱动的应用中以这样或那样的形式广泛采用。reduce()
操作为Stream API配备了类似的折叠能力。
因此,如果你有一些int
,比如说[11, 22, 33, 44, 55]
,你可以使用reduce()
,找到它们的总和,以及其他结果。
在函数式编程中,寻找这些数字的总和将应用这样的步骤。
0 + 11 = 11
11 + 22 = 33
33 + 33 = 66
66 + 44 = 110
110 + 55 = 165
使用reduce()
的方法,可以达到这样的效果。
int[] values = new int[]{11, 22, 33, 44, 55};
IntStream stream = Arrays.stream(values);
int sum = stream.reduce(0, (left, right) -> left + right);
sum
165
reduce()
是很直接的。例如,如果你看一下函数式例程,你可以把+
运算符左边的所有数值称为left
;而右边的数值称为right
。然后,在每次求和操作之后,结果就成为下一次求和的新left
。
同样,Java的reduce()
方法所做的与函数例程完全一样。它甚至包括一个起始值,0
,而函数例程也有这个起始值。
从操作上看,reduce()
方法将一个left
的值加到下一个right
的值上。然后,它把这个值加到下一个right
...以此类推。
你甚至可以把reduce()
实现对这些值的折叠想象成。
((((0 + 11) + 22) + 33) + 44) + 55 = 165
不过,Stream API并不像上面的例子那样只提供reduce()
的折叠功能。
它全力以赴地将其功能接口包含在三个reduce()
方法的实现中。正如你将在随后的章节中看到的那样,API提供的reduce()
,其口味如下。
T reduce(T identity, BinaryOperator<T> accumulator)
这个版本是我们之前使用的。其中,
0
是identity
;而(left, right) -> left + right)
是实现了BinaryOperator
功能接口的accumulator
。
还有
Optional<T> reduce(BinaryOperator<T> accumulator)
还有
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BinaryOperator<U> combiner)
**注意:**Stream API的sum()
、average()
、max()
和min()
操作是还原变化,等同于调用。
// Equivalent to stream.sum()
stream.reduce(0, Integer::sum);
// Equivalent to stream.max()
stream.reduce(0, Integer::max);
// Equivalent to stream.min()
stream.reduce(0, Integer::min);
在接下来的章节中,我们将深入研究
reduce()
方法、它的变体、用例和良好实践,让你对底层机制有更深的理解和体会。
reduce()的种类和例子
Stream API提供了三种reduce()
操作的变体。让我们来看看它们各自的定义和实际用法。
1.reduce(),其结果与流元素的类型相同
方法签名
T reduce(T identity, BinaryOperator<T> accumulator)
使用所提供的标识值和关联累加函数,对这个流的元素进行还原,并返回还原后的值。
现在,我们知道了这种类型的reduce()
的操作方式。但是,在使用这种reduce()
类型时,有一个小问题你应该小心。(实际上,对于任何减少操作)。
你的
reduce()
实现的关联性。
当你使用reduce()
,你应该为你的例程也提供在并行设置中运行的可能性。还原操作不受限制,可以按顺序执行。
为此,关联性是至关重要的,因为它将使你的累加器产生正确的结果,无论流元素的相遇顺序如何。如果关联性在这里不成立,累加器就不可靠了。
案例:比如,你有三个int
值,[8, 5, 4]
。
关联性要求以任何顺序对这些值进行操作都应该产生匹配的结果。比如说。
(8 + 5) + 6 == 8 + (5 + 6)
另外,当并行化发生时,累积可能以更小的单位处理这些值。例如,拿一个包含数值[7, 3, 5, 1]
的流来说。一个并行的流可能会使累加以这样的方式工作。
7 + 3 + 5 + 1 == (7 + 3) + (5 + 1)
但是,这些要求有效地禁止你用reduce()
方法进行某些类型的操作。例如,你不能用reduce()
进行减法运算。这是因为它违反了关联性原则。
你看,如果你使用前面一个例子中的数值。[8, 5, 4]
.然后试图用reduce()
来求出它们的累积之差。
结果会是这样的
(8 - 5) - 6 != 8 - (5 - 6)
否则,身份参数是另一个需要注意的因素。选择一个身份值,i
,这样:对于流中的每个元素e
,对其进行操作应该总是返回 。 *op
*应该总是返回e
。
这意味着什么?
e op identity = e
在加法的情况下,身份是
0
。在乘法的情况下,身份是1
(因为与0的乘法总是0,不是e)。在字符串的情况下,身份是String
,等等。
这种操作在Java中的功能是:
IntStream intStream = IntStream.of(11, 22, 33, 44, 55);
Stream stringStream = Stream.of("Java", "Python", "JavaScript");
int sum = intStream.reduce(0, (left, right) -> left + right);
int max = intStream.reduce(0, Integer::max);
int min = intStream.reduce(0, Integer::min);
// Mapping elements to a stream of integers, thus the return type is the same type as the stream itself
int sumOfLengths = stringStream.mapToInt(String::length)
.reduce(0, Integer::sum);
这些reduce()
的调用非常普遍,所以它们被更高层次的调用所取代--sum()
,min()
,max()
, 你完全可以用这些调用来代替reduce()
的调用,不过请记住,它们被修改为返回Optional
的变体。
int sum = intStream.sum();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();
reduce()
的闪光点是在你想从任何序列中得到任何标量结果的情况下--比如将一个集合减少到一个具有最大长度的元素,这将导致一个Optional
。我们现在将看一下这个。
2.reduce(),其结果是一个可选项
方法签名
Optional<T> reduce(BinaryOperator<T> accumulator)
对这个流的元素进行缩减,使用一个关联的累积函数,并返回一个描述缩减值的Optional,如果有的话。
在操作上,这是使用reduce()
方法的最简单的方式。它只要求一个参数。一个BinaryOperator
实现,它将作为一个累加器。
所以,与其这样,不如这样。
int sum = stream
.reduce(0, (left, right) -> left + right);
你只需要这样做(即省去身份值)。
Optional<Integer> sum = stream
.reduce((left, right) -> left + right);
前者和后者的区别在于,在后者中,结果可能不包含任何值。
例如,当你传递一个空流进行评估时就会发生这种情况。然而,当你使用一个身份作为参数之一时,这种情况就不会发生,因为当你向它提供一个空流时,reduce()
会返回身份本身作为结果。
另一个例子是将集合减少到某些元素,比如将几个字符串创建的流减少到一个单一的元素。
List<String> langs = List.of("Java", "Python", "JavaScript");
Optional longest = langs.stream().reduce(
(s1, s2) -> (s1.length() > s2.length()) ? s1 : s2);
这里发生了什么?我们正在流化一个列表并将其还原。对于每两个元素(s1, s2
),它们的长度被比较,根据结果,使用三元操作符,返回s1
或s2
。
具有最大长度的元素将通过这些调用传播,缩减的结果是它被返回并打包成一个Optional
,如果这样的元素存在的话。
longest.ifPresent(System.out::println);
这就导致了
JavaScript
3.reduce(),使用一个组合函数
方法签名
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
对这个流的元素进行还原,使用提供的身份、累积和组合函数。
虽然这个定义看起来很直接,但它隐藏了一个强大的能力。
这个
reduce()
变体可以让你处理一个类型与流的元素不匹配的结果。
我们以前没有这样做过吗?并非如此。
int sumOfLengths = stringStream
.mapToInt(String::length)
.reduce(0, Integer::sum);
mapToInt()
方法返回一个IntStream
,所以即使我们一开始是一个字符串流 -reduce()
方法是在一个IntStream
上调用的,并返回一个整数,这就是流中元素的类型。
mapToInt()
是一个快速的黑客,它允许我们 "返回一个不同的类型",尽管它并没有真正返回一个不同的类型。
就拿你想计算一段话的累积长度的情况来说,或者像我们之前的那样计算单词的长度。
这表明你可能有一个String
的元素流。然而,你需要reduce()
操作的返回类型有一个int
的值来表示该段的长度。
这就是组合器开始发挥作用的地方。
String string = "Our Mathematical Universe: My Quest for the Ultimate Nature of Reality";
List<String> wordList = List.of(string.split(" "));
int length = wordList
.stream()
.reduce(
0,
(parLength, word) -> parLength + word.length(),
(parLength, otherParLength) -> parLength + otherParLength
);
System.out.println(String.format("The sum length of all the words in the paragraph is %d", length));
这段代码将段落中所有字符串的长度相加,细分到每个空格(因此计算中不包括空格),结果是。
The sum length of all the words in the paragraph is 60
这个reduce()
变体值得注意的特点是,它很好地服务于并行化。
以本例中的累加器为例。
(parLength, word) -> parLength + word.length()
reduce()
操作会多次调用它,这是毫无疑问的。然而,在一个并行化的流中,管道中可能最终会有相当多的累加器。这就是组合器函数的作用。
本例中的组合器函数是
(parLength, otherParLength) -> parLength + otherParLength
它将可用的累加器的结果相加,产生最后的结果。
这使得reduce()
,将一个庞大的过程分解成许多更小的、可能更快的操作。这也让我们进入了下一个重要的话题--并行化。
在并行流中使用reduce()
你可以通过调用parallel()
方法将任何顺序流变成一个并行的。
同样,让我们考虑一个用例,即你想对给定范围内的所有int
值进行求和,以测试reduce()
是如何并行工作的。
有几种方法可以使用流API在给定的范围内生成一个int 值的序列。
- 使用
Stream.iterate
- 使用
IntStream.rangeClosed
使用Stream.iterate()
private final int max = 1_000_000;
Stream<Integer> iterateStream = Stream.iterate(1, number -> number + 1).limit(max);
使用IntStream.rangeClosed()
IntStream rangeClosedStream = IntStream.rangeClosed(1, max);
那么,如果我们有这两种产生int
值流的方法,对于我们的用例来说,是否有一种方法比另一种方法更有效?
答案是响亮的
当你对它们应用reduce()
操作时,Stream.iterate()
的效率就不如IntStream.rangeClosed()
。我们很快就会看到原因。
当你使用这两种策略来寻找数字的总和时,你会写这样的代码。
Integer iterateSum = iterateStream
.parallel()
.reduce(0, (number1, number2) -> number1 + number2);
int rangeClosedSum = rangeClosedStream
.parallel()
.reduce(0, (number1, number2) -> number1 + number2);
诚然,这两种方式总是会产生匹配和正确的结果。
例如,如果你将变量max
设为1,000,000
,你将从两种reduce()
方法中得到1,784,293,664
。
然而,计算iterateSum
要比rangeClosedSum
慢。
造成这种情况的原因是,Stream.iterate()
对其管道中遇到的所有数字值都进行了拆箱和装箱。例如,请注意,我们向它提供了int
的值,它的结果是返回一个Integer
对象。
IntStream.rangeClosed()
而GitHub的 "Single "则没有这个缺点,因为它直接处理了 ,甚至还返回了一个 的结果。int
int
下面是GitHub上的一些测试,说明了这种现象。克隆该 repo 并运行这些测试,以进一步探索
reduce()
在Stream.iterate()
和IntStream.rangeClosed()
中运行时的表现。
何时不使用reduce()
reduce()
操作需要使用一个无状态和无干扰的累积器。
这意味着,累加器最好是不可变的。而且,为了实现这一点,大多数累加器都会创建新的对象来保存下一次累加的值。
以一个案例为例,你想把几个元素的String
对象连接成一个String
对象。比如说,你想把几个词组成一个句子的地方。或者,甚至是通过连锁几个char
值来组成一个词。
官方文档提供了一个这样的例子
String concatenated = strings.reduce("", String::concat);
在这里,如果strings
流有大量的元素,reduce()
操作将创建非常多的字符串对象。
而且,根据strings
流的大小,由于所有正在进行的对象分配,性能将快速下降。
为了更清楚地了解这个操作是如何工作的,请考虑它的for
循环等效。然后,注意新的String
对象是如何在每个循环通道中实现的。
String concatenated = "";
for (String string : strings) {
concatenated += string;
}
然而,你可以尝试通过首先使用可变对象来补救在
reduce()
操作中创建新对象的问题。
然而,请记住,如果你试图通过使用像List
这样的可变身份容器来弥补这一缺陷,我们就会将该容器暴露在ConcurrentModification
异常中。
以一个案例为例,你想把reduce()
一个int
的值流变成一个List
的Integer
对象。你可以做这样的事情。
Stream<Integer> numbersStream = Arrays.asList(12, 13, 14, 15, 16, 17).stream();
List<Integer> numbersList = numbersStream.reduce(
// Identity
new ArrayList<>(),
// Accumulator
(list, number) -> {
list.add(number);
return list;
},
// Combiner
(list1, list2) -> {
list1.addAll(list2);
return list1;
}
);
这段代码会给你一个正确的结果
[12, 13, 14, 15, 16, 17]
但是,它要付出一定的代价。
首先,本例中的累加器干扰了身份识别。它引入了一个副作用,在作为身份的列表中增加了一个值。
然后,如果你碰巧把流,numbersStream
,变成一个并行的,你就会把列表累积暴露在并发的修改中。而且,这势必会使操作在某些时候抛出一个ConcurrentModification
。
因此,你的整个reduce()
操作可能完全失败。
将*reduce()*付诸实践
由于其功能性质,Stream API要求我们对如何设计Java代码进行全面的重新思考。它要求使用的方法能够符合诸如reduce()
等操作的功能接口的模式。
因此,我们将设计我们的代码,以便当我们对其调用reduce()
操作时,会产生简洁的代码。比如说,你可以用成员引用重写。
但是,首先,让我们探讨一下我们将用来测试reduce()
操作的用例。
- 我们有一家杂货店,出售各种产品。例如,奶酪、西红柿和黄瓜。
- 现在,每个产品都有属性,如名称、价格和单位重量。
- 顾客通过交易从商店获得产品。
作为这样一家杂货店的经理,有一天你进来,问店员几个问题。
- 你从所有的交易中赚了多少钱?
- 卖出去的东西有多重?也就是说,你卖出的产品的累计重量是多少?
- 顾客支付最多的交易的价值是多少?
- 哪笔交易的价值最低(就其总价格价值而言)?
设计领域
我们将创建一个类Product
,以表示将那家杂货店将储存的物品。
public class Product {
private final String name;
private final Price price;
private final Weight weight;
public Product(String name, Price price, Weight weight) {
this.name = name;
this.price = price;
this.weight = weight;
}
// Getters
}
请注意,我们将两个价值类作为Product
的字段,名为Weight
和Price
。
然而,如果我们想天真地做,我们会让这两个字段的值为double
。
像这样
public Product(String name, double price, double weight) {
this.name = name;
this.price = price;
this.weight = weight;
}
这样做有一个绝对好的理由,你很快就会发现原因。否则,Price
和Weight
都是对double
值的简单包装。
public class Price {
private final double value;
public Price(double value) {
this.value = value;
}
//Getters
}
public class Weight {
private final double value;
public Weight(double value) {
this.value = value;
}
// Getters
}
然后,我们有Transaction
类。这个类将包含一个Product
和int
值,该值代表了客户将购买的产品的数量。
Product
因此,Transaction
应该能够告知我们客户购买的Price
和Weight
的总数。因此,它应该包括这样的方法。
public class Transaction {
private final Product product;
private final int quantity;
public Transaction(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
//Getters ommited
public Price getTotalPrice() {
return this.product.getPrice().getTotal(quantity);
}
public Weight getTotalWeight() {
return this.product.getWeight().getTotal(quantity);
}
}
请注意getTotalPrice()
和getTotalWeight()
方法是如何将其计算委托给Price
和Weight
的。
这些委托是相当重要的,这也是我们使用类而不是简单的
double
字段的原因。
它们表明,Price
和Weight
应该能够对它们的类型进行累积。
请记住,reduce()
操作总是以BinaryOperator
作为其累加器。所以,这就是我们开始为我们的类预先建立累加器的关口。
因此,添加以下方法,作为Price
和Weight
的累加器。
public class Price {
// Fields, constructor, getters
public Price add(Price otherPrice) {
return new Price(value + otherPrice.getValue());
}
public Price getTotal(int quantity) {
return new Price(value * quantity);
}
}
public class Weight {
// Fields, constructor, getters
public Weight add(Weight otherWeight) {
return new Weight(value + otherWeight.getValue());
}
public Weight getTotal(int quantity) {
return new Weight(value * quantity);
}
}
reduce()
操作有一些变体,也需要身份参数。由于身份是一个计算的起点(可能是具有最低值的对象),我们应该继续创建Price
和Weight
的身份版本。
你可以通过简单地将这些类的身份版本作为全局变量来做到这一点。因此,让我们把名为NIL
的字段添加到Price
和Weight
。
public class Price {
// Adding NIL
public static final Price NIL = new Price(0.0);
private final double value;
public Price(double value) {
this.value = value;
}
}
public class Weight {
// Adding NIL
public static final Weight NIL = new Weight(0.0);
private final double value;
public Weight(double value) {
this.value = value;
}
}
正如名字NIL
所示,这些字段代表Price
或Weight
,它具有最小的值。完成这些后,现在是时候创建将进行交易的Grocery
对象了。
public class Grocery {
public static void main(String[] args) {
//Inventory
Product orange = new Product("Orange", new Price(2.99), new Weight(2.0));
Product apple = new Product("Apple", new Price(1.99), new Weight(3.0));
Product tomato = new Product("Tomato", new Price(3.49), new Weight(4.0));
Product cucumber = new Product("Cucumber", new Price(2.29), new Weight(1.0));
Product cheese = new Product("Cheese", new Price(9.99), new Weight(1.0));
Product beef = new Product("Beef", new Price(7.99), new Weight(10.0));
//Transactions
List<Transaction> transactions = Arrays.asList(
new Transaction(orange, 14),
new Transaction(apple, 12),
new Transaction(tomato, 5),
new Transaction(cucumber, 15),
new Transaction(cheese, 8),
new Transaction(beef, 6)
);
}
}
如代码所示,Grocery
,它的库存中有几个Product
对象。而且,发生了几个Transaction
事件。
不过,商店的经理还是要求提供一些关于交易的数据。因此,我们应该着手让reduce()
,以帮助我们回答这些询问。
从所有交易中赚到的钱
所有交易的总价格是所有交易的总价格相加的结果。
因此,我们首先将map()
所有的Transaction
元素变成它们的Price
值。
然后,我们将Price
元素减少为其价值的总和。
在这里,将累加器抽象为Price
对象本身,使代码具有高度可读性。同时,包含了Price.NIL
的身份,使reduce()
的操作尽可能具有可读性。
Price totalPrice = transactions.stream()
.map(Transaction::getTotalPrice)
.reduce(Price.NIL, Price::add);
System.out.printf("Total price of all transactions: %s\n", totalPrice);
运行该代码片段后,你应该期待的输出是。
Total price of all transactions: $245.40
也请注意,我们将打印价格值的工作委托给Print
对象的toString()
方法,以进一步简化调试。
使用
toString()
方法为对象的价值提供人性化的描述,总是好的做法。
@Override
public String toString() {
return String.format("$%.2f", value);
}
所有售出产品的总重量
与我们对Price
所做的类似,这里我们让Weight
负责对几个元素的值进行求和。
当然,我们需要map()
管道中的每个Transaction
元素到一个Weight
对象。
然后,我们给Weight
元素布置任务,让它们自己进行值的累加。
Weight totalWeight = transactions.stream()
.map(Transaction::getTotalWeight)
.reduce(Weight.NIL, Weight::add);
System.out.printf("Total weight of all sold products: %s\n", totalWeight);
在运行这个片段时,你应该有这样的输出。
Total weight of all sold products: 167.00 lbs
最高价值交易的价格
这个查询要求对Price
找到两个Price
元素之间的最小值或最大值的方式进行一些重新设计。
请记住,在前面的任务中,我们所做的只是在执行reduce()
时累积数值。然而,找到一个最小或最大的值则完全是另一回事。
我们在之前的累积中做了求和,而在这里我们必须从第一个Price
元素的值开始。然后我们将用另一个值来代替它,如果该值大于我们所拥有的值。因此,最后我们会得到最高值。这个逻辑也适用于你寻求最小值的时候。
因此,包括这个代码来计算你的最大和最小值的Price
元素。
public class Price {
// Fields, getters, constructors, other methods
public Price getMin(Price otherPrice){
return new Price(Double.min(value, otherPrice.getValue()));
}
public Price getMax(Price otherPrice){
return new Price(Double.max(value, otherPrice.getValue()));
}
}
而当你在你的Grocery
对象计算中包括这些能力时,你将得到一个reduce()
操作,看起来像这样。
transactions.stream()
.map(Transaction::getTotalPrice)
.reduce(Price::getMax)
.ifPresent(price -> System.out.printf("Highest transaction price: %s\n", price));
输出为
Highest transaction price: $79.92
还要注意的是,我们使用了
reduce()
变体,它只需要一个参数:一个BinaryOperator
。我们的想法是:我们不需要一个身份参数,因为我们不需要这个操作的默认起点。
当你从一个元素的集合中寻求最大值时,你直接开始测试这些元素,而不涉及任何外部默认值。
最低值交易
继续我们在前面的任务中开始的趋势,我们把关于哪个是最低值事务的查询委托给Transaction
元素本身。
此外,因为我们需要一个包含整个Transaction
元素的细节的结果,我们把所有的查询都指向Transaction
元素流,而不把它们映射到任何其他类型。
不过,要使一个Transaction
元素以Price
来衡量它的价值,还是有一些工作要做的。
首先,你需要找到两个Transaction
对象的最小Price
。
然后,检查哪个Transaction
有这个最小的Price
,并返回它。
否则,你将通过使用一个例程,如这个getMin
方法来完成这个任务。
public class Transaction {
// Fields, getters, constructors, other methods
public Transaction getMin(Transaction otherTransaction) {
Price min = this.getTotalPrice().getMin(otherTransaction.getTotalPrice());
return min.equals(this.getTotalPrice()) ? this : otherTransaction;
}
}
完成这些后,将该例程纳入到一个reduce()
的操作中就变得相当简单了,比如这个。
transactions.stream()
.reduce(Transaction::getMin)
.ifPresent(transaction -> {
System.out.printf("Transaction with lowest value: %s\n", transaction);
});
为了获得一个输出
Transaction with lowest value { Product: Tomato; price: $3.49 Qty: 5 lbs Total price: $17.45}
同样,当你完全利用
toString()
,就可以获得这样的输出。使用它来生成尽可能多的信息,以便在你打印对象时使其价值对人友好。
结论
作为Java的普通折叠例程的实现,reduce()
是相当有效的。然而,正如我们所看到的,它要求你完全重新考虑如何设计你的类,以便能够充分地利用它。
但是请记住,如果你错误地使用reduce()
,会降低你的代码的性能。该操作在顺序流和并行流中都能工作。然而,当你把它用在巨大的流中时,会变得很棘手,因为reduce()
在可变的减少操作中并不高效。
例如,我们看到一个案例,你可以用reduce()
来串联String
的元素。记住String
对象是不可变的。因此,当我们使用reduce()
进行累加时,我们实际上在每次累加时都会创建非常多的String
对象。
然而,如果你试图通过使用像List
这样的可变身份容器来弥补这一缺陷,我们就会将该容器暴露在ConcurrentModification
异常中。
否则,我们已经探索了一个杂货店的交易的用例。我们为这个场景设计了代码,使每个积累都进行小而快的计算。
是的,对于我们用reduce()
调用的每一个积累,新的对象分配仍然存在。但是,我们已经使它们尽可能的简单。因此,当你将Transaction
流并行化时,我们的实现也能很好地工作。
。
推荐阅读
-
Java中的 JDK8、JDK11、JDK17 该如何选择?
-
为什么UTF-8 和 GBK 会相互转换,为什么会一团糟?-锟斤拷 "是指在字节和字符的转换(编码和解码)过程中使用了不同的编码,找出编码和解码的编码,修改后使用同一种编码。 ===================== 补充 ========================== 在上面的文章中,其实一直回避了一个问题,那就是既然保存中的所有字符都需要转换成二进制,那么 java 是使用什么编码来保存字符的呢?这个问题我们其实可以不必深究,因为它对我们来说是透明的,我们只需假定 java 使用了某种可以表示所有字符的编码。由于这种透明性,我们可以假设 java 直接保存字符本身,就像上面所说的那样。 在 java 虚拟机中使用的是 unicode 字符集。
-
Java 8 之后的那些新功能(4):网络请求 Java Http 客户端
-
Java8 的特点是,使用 Stream 流,将其汇集成地图集合
-
谈谈 Java 8 中引入的流 API
-
为 VScode + Java 8 配置 Java 开发环境的步骤
-
Java 17 VS Java 8:新旧对比,您不想错过的 Java 17 功能 - III,语言功能对比
-
[翻译] Java 17 的功能:对比版本 8 和 17,这些年来有哪些变化?
-
Java 中 8 种基本数据类型的 4 个类别
-
Nacos 无法启动详解:请在您的环境中设置 JAVA_HOME 变量,我们需要 java(x64) jdk8 或更高版本。