接上篇:大问题1 | Array最全源码分析(上)内部工具和数组排序
5.并行前缀计算
首先解释一下并行前缀计算是做什么,我按照源码中的注释直译:
使用提供的函数,对给定数组的每个元素进行并行累积操作,并就地修改数组。
- 例如,如果数组初始时包含 [2, 1, 0, 3],并且操作执行加法,
- 那么返回时数组将包含 [2, 3, 3, 6]。
- 对于大型数组,并行前缀计算通常比顺序循环更有效率。
这个方法的关键点如下:
<T>
:这是一个泛型参数,表示数组中元素的类型。array
:这是要进行并行前缀运算的数组,数组中的元素会被就地修改,也就是说,运算的结果会直接反映在输入的数组上。op
:这是一个二元操作符,它是一个函数接口BinaryOperator<T>
的实例,用于定义如何对数组中的元素进行累积操作。这个函数必须是无副作用的(不改变外部状态)并且满足结合律(操作的顺序不影响结果)。new ArrayPrefixHelpers.CumulateTask<>(null, op, array, 0, array.length).invoke();
:这行代码创建了一个CumulateTask
对象,它是一个用于并行计算的任务。CumulateTask
是ArrayPrefixHelpers
类的一个内部类。创建任务时,传入了操作符op
、数组array
以及要处理的数组范围(从索引0到array.length
)。调用invoke()
方法会开始执行并行计算。
和排序方法相同,此方法也能指定数组的范围,下附源码:
public static <T> void parallelPrefix(T[] array, BinaryOperator<T> op) {
// 检查提供的函数是否为null,如果为null则抛出NullPointerException异常
Objects.requireNonNull(op);
// 如果数组长度大于0,则执行并行累积任务
if (array.length > 0)
new ArrayPrefixHelpers.CumulateTask<>
(null, op, array, 0, array.length).invoke();
}
public static <T> void parallelPrefix(T[] array, int fromIndex,
int toIndex, BinaryOperator<T> op) {
// 检查提供的函数是否为null,如果为null则抛出NullPointerException异常
Objects.requireNonNull(op);
// 检查索引范围是否合法,如果不合法则抛出相应的异常
rangeCheck(array.length, fromIndex, toIndex);
// 如果子范围有效(起始索引小于结束索引),则执行并行累积任务
if (fromIndex < toIndex)
new ArrayPrefixHelpers.CumulateTask<>
(null, op, array, fromIndex, toIndex).invoke();
}
此处注意,BinaryOperator<T>
:
- 这是一个泛型接口,可以用于任何类型的对象。
- 它的方法签名是
T apply(T t1, T t2)
,意味着它接受两个类型为T
的参数,并返回一个同样类型为T
的结果。 - 由于
BinaryOperator<T>
是泛型的,它可以用于对象类型,如Integer
、String
等,但如果用于原始类型,就会涉及到自动装箱和拆箱操作,这可能会影响性能。
而对于原始类型,源码中也提供了对应的特化版本,如对于原始long
类型的操作,可以使用LongBinaryOperator
是,如果你需要对对象类型进行操作,或者你的操作逻辑不局限于原始long
类型,那么应该使用BinaryOperator<T>
。
其他原始数据类型同上,不在此赘述了。
6. 二分查找
Java的数组操作方法之一,用于在一个已排序的数组中使用二分查找算法来搜索指定的值。
binarySearch(long[] a, long key)
方法内部调用了binarySearch0(long[] a, int fromIndex, int toIndex, long key)
方法,这是一个私有辅助方法,用于实际执行二分查找。这里,fromIndex
和toIndex
分别表示查找范围的起始和结束索引,在这个公共方法中,查找范围是整个数组,所以fromIndex
是0,toIndex
是数组的长度a.length
。
注意,传入的数组要求是已排序过的,同样,二分查找也支持指定范围的变体。
我们看源码:
public static int binarySearch(long[] a, long key) {
return binarySearch0(a, 0, a.length, key);
}
public static int binarySearch(long[] a, int fromIndex, int toIndex,
long key) {
rangeCheck(a.length, fromIndex, toIndex);
return binarySearch0(a, fromIndex, toIndex, key);
}
// Like public version, but without range checks.
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
long midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
源码中我们可以看到binarySearch0方法的实现,我们简单过一下流程:
low
变量初始化为搜索范围的起始索引fromIndex
。high
变量初始化为搜索范围的结束索引toIndex
减去1,因为toIndex
是不包含在搜索范围内的。- 当
low
小于或等于high
时,循环继续执行,这是二分查找算法的核心循环。 - 在循环中,计算中间索引
mid
,这里使用(low + high) >>> 1
来避免整数溢出的问题。 - 根据中间索引
mid
取出数组中的值midVal
。 - 如果
midVal
小于要查找的值key
,说明key
应该在mid
的右侧,所以将low
设置为mid + 1
。 - 如果
midVal
大于key
,说明key
应该在mid
的左侧,所以将high
设置为mid - 1
。 - 如果
midVal
等于key
,则找到了要查找的值,返回中间索引mid
。 - 如果循环结束还没有找到
key
,则返回-(low + 1)
,这表示key
不存在于数组中,且low
是key
应该插入的位置。
此处有几点可能需要注意:
-
在计算中间索引时,如果直接使用
(low + high) / 2
,当low
和high
都是非常大的正整数时,它们的和可能会超过int
类型能表示的最大值(Integer.MAX_VALUE
),导致整数溢出。当整数溢出发生时,结果会变成一个负数,这将导致算法失败。为了避免这个问题,可以使用无符号右移操作符
>>>
来代替除以2。使用无符号右移来计算中点是一种常见的技巧,可以确保即使在极端情况下也不会因为整数溢出而导致算法失败
-
在二分查找算法中,如果没有找到指定的
key
,low
变量将指向key
应该插入的位置。这是因为在查找过程中,low
和high
两个指针不断地调整以缩小搜索范围,直到它们相遇或者low
超过high
。这种情况发生时,
low
指针所在的位置就是第一个大于key
的元素的索引,或者如果所有元素都小于key
,则low
将等于toIndex
(搜索范围的结束索引)。
对于其他基本类型的支持,我就不再继续赘述了,除了有一个特殊的点,对于Double和Float这种浮点类型的特殊处理,下附源码:
private static int binarySearch0(double[] a, int fromIndex, int toIndex,
double key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
double midVal = a[mid];
if (midVal < key)
low = mid + 1; // Neither val is NaN, thisVal is smaller
else if (midVal > key)
high = mid - 1; // Neither val is NaN, thisVal is larger
else {
long midBits = Double.doubleToLongBits(midVal);
long keyBits = Double.doubleToLongBits(key);
if (midBits == keyBits) // Values are equal
return mid; // Key found
else if (midBits < keyBits) // (-0.0, 0.0) or (!NaN, NaN)
low = mid + 1;
else // (0.0, -0.0) or (NaN, !NaN)
high = mid - 1;
}
}
return -(low + 1); // key not found.
}
有朋友可能会疑惑,为什么此处和刚刚看到的long类型不同,使用了Double.doubleToLongBits
方法。
在Java中,Double.doubleToLongBits
方法用于将double
类型的浮点数的位模式转换为long
类型的整数表示。这个方法对于处理double
类型的特殊值非常有用,特别是在需要精确比较两个double
值时。
浮点数在计算机中的表示遵循IEEE 754标准,这个标准定义了正零(+0.0)和负零(-0.0)以及NaN(非数字)的表示方式。在Java中,double
类型的正零和负零虽然在数值上被认为是相等的,但它们在位模式上是不同的。同样,NaN也有多种可能的位模式表示。
当使用==
运算符比较两个double
值时,正零和负零被认为是相等的,而任何NaN值与任何值(包括它自己)的比较都将返回false
。然而,在某些情况下,我们可能需要区分正零和负零,或者检测一个值是否为NaN。
Double.doubleToLongBits
方法提供了一种方式来进行这种精确的比较:
- 它将
double
值的位模式转换为一个long
整数,这样就可以比较这些位模式来确定两个double
值是否完全相同。 - 对于正零和负零,它们的位模式不同,因此转换后的
long
值也会不同,使得我们能够区分它们。 - 对于NaN值,
Double.doubleToLongBits
方法将所有NaN值的位模式规范化为同一个long
值,这样就可以检测一个值是否为NaN,即使它们的原始位模式可能不同。
在二分查找的上下文中,使用Double.doubleToLongBits
可以确保我们在比较两个double
值时,不仅比较它们的数值大小,而且还比较它们的确切位模式。这是必要的,因为二分查找需要确定一个确切的匹配位置,而不是仅仅数值上的相等。
类似的,float类型使用Float.floatToIntBits
比较。
同样的,针对指定的对象类型和泛型类型也可以使用二分查找,与之前类似,此处也不再赘述。
7. 判断是否相等
以long数组的比较为例,先看源码:
public static boolean equals(long[] a, long[] a2) {
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
return true;
}
这个方法接受两个参数,a
和a2
,它们都是long
类型的数组。方法的返回类型是boolean
,即返回true
或false
来表示两个数组是否相等。
方法的逻辑如下:
- 首先,方法检查两个数组引用是否指向同一个对象。如果是,那么它们肯定相等,方法返回
true
。 - 接下来,方法检查任何一个数组是否为
null
。如果其中一个数组为null
而另一个不是,那么它们不可能相等,方法返回false
。 - 然后,方法比较两个数组的长度。如果它们的长度不同,那么它们不可能相等,方法返回
false
。 - 如果两个数组的长度相同,方法将遍历数组中的元素。使用一个
for
循环,从索引0
开始,一直到数组的长度减去1
的位置。 - 在循环中,方法比较两个数组在相同索引位置上的元素。如果在任何位置上发现两个数组的元素不相等(即
a[i]
不等于a2[i]
),那么两个数组不相等,方法返回false
。 - 如果循环结束后没有发现不相等的元素,那么两个数组在所有对应位置上的元素都相等,方法返回
true
。
其他类型亦复如是。
double和float的特殊处理和对象数组的处理也不再赘述了。
注意一个点:当两个对象引用都等于null
时,使用==
运算符比较它们一定会返回true
。这是因为null
在Java中表示一个引用不指向任何对象。当你比较两个引用是否相等时,==
运算符实际上是在检查两个引用是否指向内存中的同一个位置。
在null
的情况下,由于它们都不指向任何对象,可以认为它们指向了一个“空”的位置。
8. 填充数组
将指定的某个类型值填充到一个该类型数组的每个元素中。
看源码,还是以long类型为例:
public static void fill(long[] a, long val) {
for (int i = 0, len = a.length; i < len; i++)
a[i] = val;
}
方法的行为:
- 方法通过一个
for
循环遍历数组a
的每个元素。 - 在循环中,变量
i
从0开始,一直增加到数组的长度len
(a.length
),但不包括len
。 - 在每次循环迭代中,将
val
赋值给数组a
当前索引i
处的元素。
这种操作在初始化数组或者需要将数组的所有元素重置为同一个值时非常有用。
其他类型和指定范围亦复如是。
9. 复制数组
之前讲过,在本文中就不复述了,详见:https://blog.csdn.net/dongangsta/article/details/136435679
也支持指定范围和不同类型、对象、泛型。
10. 创建列表
获取指定的元素创建的列表,源码如下:
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
/**
* @serial include
*/
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
@Override
public int indexOf(Object o) {
E[] a = this.a;
if (o == null) {
for (int i = 0; i < a.length; i++)
if (a[i] == null)
return i;
} else {
for (int i = 0; i < a.length; i++)
if (o.equals(a[i]))
return i;
}
return -1;
}
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(a, Spliterator.ORDERED);
}
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : a) {
action.accept(e);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
E[] a = this.a;
for (int i = 0; i < a.length; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
public void sort(Comparator<? super E> c) {
Arrays.sort(a, c);
}
}
在此处我们能注意到,这个ArrayList是在Arrays.java包下自定义了一个ArrayList类,这个类并未实现add()等方法,因此——
使用该方法时,不能对转化后的List进行增加或删除操作,只能进行读取或修改。
此外,还有两点需要我们注意:
-
不要对基本数据类型数组进行转换,尽量使用基本数据类型的包装类,如:Integer等。
如下例,由于int[]本身就是一个类型,所以编译器认为只传了一个变量,这个变量的类型是int数组,结构为int(1)(),因此size为1。
public class AsListTest {
public static void main(String[] args) {
int[] i = {1, 2, 3};
List list = Arrays.asList(i);
System.out.println(list.size());
}
}
// 结果:1
- 对List或Array其中一个进行修改,另一个也会相应改变
public class AsListTest {
public static void main(String[] args) {
String[] s = {"a", "b", "c"};
List list = Arrays.asList(s);
s[0] = "b";
System.out.println("Array:"+Arrays.toString(s));
System.out.println("List:"+list);
list.set(1, "c");
System.out.println("Array:"+Arrays.toString(s));
System.out.println("List:"+list);
}
}
// 结果:
// Array:[b, b, c]
// List:[b, b, c]
// Array:[b, c, c]
// List:[b, c, c]
这是因为由asList()方法生成的List仅是对Array进行了一层包装,对List进行操作实际还是对初始Array的操作。
11. 计算哈希码
计算一个数组的哈希码。如果两个数组a和b内容相同(即Arrays.equals(a, b)返回true),那么这两个数组的哈希码也应该相同(即Arrays.hashCode(a) == Arrays.hashCode(b))。
以int类型的数组为例,源码如下:
public static int hashCode(int a[]) {
if (a == null)
return 0;
int result = 1;
for (int element : a)
result = 31 * result + element;
return result;
}
以下是对代码运行过程的解释:
- 检查传入的数组
a
是否为null
,如果数组a
是null
,则方法直接返回0作为其哈希码。 - 如果数组
a
不是null
,则初始化一个变量result
,赋值为1,这个变量将用来累积数组中每个元素的哈希码。 - 遍历数组
a
中的每个元素,每次循环中的element
变量代表数组中的一个元素。 result = 31 * result + element;
这行代码更新result
变量的值。它将result
先乘以31,然后加上当前元素element
。这样做是为了混合前一个元素的哈希值和当前元素的哈希值,从而为数组中的每个元素生成一个综合的哈希码。
注意,对于long
类型,因为long
类型的值是64位的,而int
类型的值是32位的。在Java中,hashCode()
方法返回的是一个int
类型的值,因此需要将long
类型的64位值转换成一个合适的32位的哈希码。
public static int hashCode(long a[]) {
if (a == null)
return 0;
int result = 1;
for (long element : a) {
int elementHash = (int)(element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
对于boolean类型,为了符合32位,也有特殊处理,有true是1231、flase是1237两个魔数:
for (boolean element : a)
result = 31 * result + (element ? 1231 : 1237);
对于double和float类型和之前的原因一样,为了符合32位整数的要求,需要特殊处理:
// 遍历数组a中的每个浮点元素
for (float element : a)
// 对于数组中的每个浮点元素,将当前的结果乘以31(一个质数),
// 然后加上该浮点元素的位表示转换成的整数值。
// Float.floatToIntBits(element)方法将浮点值的位表示(IEEE 754浮点数表示)转换为整数。
// 这样可以确保浮点数的位模式被用于计算哈希码。
result = 31 * result + Float.floatToIntBits(element);
对于对象类型,则调用对象类型自己的hashCode方法:
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
12. toString方法
以long数组为例,该方法首先检查传入的数组是否为null
,如果是,则直接返回字符串"null"
。接着,它计算数组的最后一个元素的索引(iMax
),如果数组为空(即长度为0),则返回表示空数组的字符串"[]"
。
如果数组非空,方法使用StringBuilder
来构建字符串。StringBuilder
是一个可变的字符序列,用于高效地构建字符串。方法开始时,先添加一个开方括号"["
到StringBuilder
。然后,它进入循环,循环体内部将数组的每个元素转换为字符串并添加到StringBuilder
中,元素之间添加逗号和空格", "
。当达到数组的最后一个元素时,它添加一个闭方括号"]"
,然后通过调用StringBuilder
的toString()
方法将构建好的字符串返回。
这种方式确保了数组的字符串表示是按照数组元素的顺序,并且格式规范,易于阅读。:
public static String toString(long[] a) {
// 如果数组a是null,返回字符串"null"
if (a == null)
return "null";
// iMax是数组最后一个元素的索引
int iMax = a.length - 1;
// 如果数组没有元素,返回"[]"
if (iMax == -1)
return "[]";
// 使用StringBuilder来构建字符串,这比字符串连接(使用+操作符)更高效
StringBuilder b = new StringBuilder();
// 开始的方括号
b.append('[');
// 遍历数组中的每个元素
for (int i = 0; ; i++) {
// 将当前元素转换为字符串并添加到StringBuilder中
// String.valueOf(long)方法用于将long类型的值转换为字符串
b.append(a[i]);
// 如果当前索引是最后一个元素的索引,添加结束的方括号并返回字符串
if (i == iMax)
return b.append(']').toString();
// 如果不是最后一个元素,添加一个逗号和一个空格,然后继续下一个元素
b.append(", ");
}
}
其他类型亦复如是。
13. 深度方法
13.1 深度哈希码
如果需要对多维数组或数组中包含的数组进行深层次的哈希码计算,应该使用Arrays.deepHashCode(Object[])
方法:
public static int deepHashCode(Object a[]) {
// 如果输入的数组a是null,直接返回0作为哈希码
if (a == null)
return 0;
// 初始化结果变量result为1,这是哈希码的初始值
int result = 1;
// 遍历数组a中的每个元素
for (Object element : a) {
int elementHash = 0; // 初始化当前元素的哈希码为0
// 根据元素的类型,选择不同的方法来计算哈希码
if (element instanceof Object[])
elementHash = deepHashCode((Object[]) element); // 对象数组,递归调用deepHashCode
else if (element instanceof byte[])
elementHash = hashCode((byte[]) element); // 字节数组,调用hashCode
else if (element instanceof short[])
elementHash = hashCode((short[]) element); // 短整型数组,调用hashCode
else if (element instanceof int[])
elementHash = hashCode((int[]) element); // 整型数组,调用hashCode
else if (element instanceof long[])
elementHash = hashCode((long[]) element); // 长整型数组,调用hashCode
else if (element instanceof char[])
elementHash = hashCode((char[]) element); // 字符数组,调用hashCode
else if (element instanceof float[])
elementHash = hashCode((float[]) element); // 浮点数数组,调用hashCode
else if (element instanceof double[])
elementHash = hashCode((double[]) element); // 双精度浮点数数组,调用hashCode
else if (element instanceof boolean[])
elementHash = hashCode((boolean[]) element); // 布尔数组,调用hashCode
else if (element != null)
elementHash = element.hashCode(); // 非数组对象,调用对象的hashCode方法
// 将当前元素的哈希码与结果变量result结合,计算新的result值
result = 31 * result + elementHash;
}
// 返回最终计算出的哈希码
return result;
}
该方法的注释指出,如果数组包含其他数组作为元素,哈希码是基于它们的内容递归计算的。因此,不应该在包含自身作为元素的数组上调用此方法,无论是直接还是间接通过多级数组,因为这样做会导致未定义的行为。
此外,返回的哈希码值等同于通过Arrays.asList(a).hashCode()
得到的值,但有一个区别:如果数组的元素e
本身是一个数组,其哈希码不是通过调用e.hashCode()
计算的,而是根据e
的类型调用相应的Arrays.hashCode(e)
(对于原始类型数组)或递归调用Arrays.deepHashCode
计算的。
13.2 深度相等
这段代码是Java中用于比较两个数组是否“深度相等”的方法。与Arrays.equals(Object[], Object[])
方法不同,deepEquals(Object[], Object[])
方法适用于嵌套数组,可以处理任意深度的数组结构。
源码如下:
public static boolean deepEquals(Object[] a1, Object[] a2) {
// 如果两个数组引用指向同一个对象,它们显然是深度相等的
if (a1 == a2)
return true;
// 如果其中一个数组是null而另一个不是,它们不是深度相等的
if (a1 == null || a2 == null)
return false;
// 如果两个数组的长度不同,它们不是深度相等的
int length = a1.length;
if (a2.length != length)
return false;
// 遍历数组中的每个元素进行比较
for (int i = 0; i < length; i++) {
Object e1 = a1[i];
Object e2 = a2[i];
// 如果当前位置的两个元素是同一个引用(包括两者都是null的情况),则继续比较下一个元素
if (e1 == e2)
continue;
// 如果当前位置的元素之一是null,数组不是深度相等的
if (e1 == null)
return false;
// 调用deepEquals0方法来确定两个元素是否深度相等
boolean eq = deepEquals0(e1, e2);
// 如果任何一对元素不深度相等,整个数组就不深度相等
if (!eq)
return false;
}
// 所有元素都深度相等,返回true
return true;
}
static boolean deepEquals0(Object e1, Object e2) {
assert e1 != null; // 断言e1不是null,因为在调用此方法之前已经检查过
boolean eq;
// 根据元素的实际类型,调用相应的方法来比较
if (e1 instanceof Object[] && e2 instanceof Object[])
eq = deepEquals((Object[]) e1, (Object[]) e2);
else if (e1 instanceof byte[] && e2 instanceof byte[])
eq = Arrays.equals((byte[]) e1, (byte[]) e2);
// ... 对于其他所有原始类型数组,也进行类似的比较
else
eq = e1.equals(e2); // 对于非数组对象,直接调用equals方法比较
return eq;
}
这种递归比较方式确保了即使数组嵌套了多层,每一层的内容都会被递归地考虑在内,从而实现深度相等的判断。
注意,如果数组包含自身作为元素(直接或间接),方法的行为是未定义的,因为这会导致无限递归。
13.3 深度toString方法
一个静态方法,用于生成一个对象数组的深层字符串表示。如果数组包含其他数组作为元素,字符串表示将包含它们的内容,依此类推。这个方法特别适用于将多维数组转换为字符串。
public static String deepToString(Object[] a) {
// 如果数组a是null,返回字符串"null"
if (a == null)
return "null";
// 初始化StringBuilder的长度,预估每个元素占用20个字符
int bufLen = 20 * a.length;
// 如果数组非空且计算的长度溢出,则将长度设置为Integer.MAX_VALUE
if (a.length != 0 && bufLen <= 0)
bufLen = Integer.MAX_VALUE;
// 创建StringBuilder用于构建字符串
StringBuilder buf = new StringBuilder(bufLen);
// 调用私有的递归方法来构建字符串
deepToString(a, buf, new HashSet<Object[]>());
// 返回构建好的字符串
return buf.toString();
}
private static void deepToString(Object[] a, StringBuilder buf,
Set<Object[]> dejaVu) {
// 如果当前数组是null,添加"null"到StringBuilder并返回
if (a == null) {
buf.append("null");
return;
}
// iMax是数组最后一个元素的索引
int iMax = a.length - 1;
// 如果数组没有元素,添加"[]"到StringBuilder并返回
if (iMax == -1) {
buf.append("[]");
return;
}
// 将当前数组添加到dejaVu集合中,用于检测自引用,避免无限递归
dejaVu.add(a);
// 开始的方括号
buf.append('[');
// 遍历数组中的每个元素
for (int i = 0; ; i++) {
Object element = a[i];
// 如果元素是null,添加"null"到StringBuilder
if (element == null) {
buf.append("null");
} else {
// 获取元素的类
Class<?> eClass = element.getClass();
// 如果元素是数组类型
if (eClass.isArray()) {
// 根据数组的具体类型调用相应的toString方法
if (eClass == byte[].class)
buf.append(toString((byte[]) element));
// ... 对于其他所有原始类型数组,也进行类似的处理
else { // 如果元素是对象数组类型
// 如果dejaVu集合中已经包含了这个元素,说明存在自引用
if (dejaVu.contains(element))
buf.append("[...]");
else
// 递归调用deepToString方法
deepToString((Object[])element, buf, dejaVu);
}
} else { // 如果元素不是数组类型,直接调用toString方法
buf.append(element.toString());
}
}
// 如果当前索引是最后一个元素的索引,退出循环
if (i == iMax)
break;
// 如果不是最后一个元素,添加逗号和空格
buf.append(", ");
}
// 添加结束的方括号
buf.append(']');
// 从dejaVu集合中移除当前数组
dejaVu.remove(a);
}
deepToString(Object[])
方法首先检查传入的数组是否为null
,如果是,则直接返回字符串"null"
。接着,它创建一个StringBuilder
实例,用于构建字符串。为了避免无限递归,方法使用一个HashSet
来跟踪已经访问过的数组对象。
在私有的deepToString
辅助方法中,它检查当前数组是否为空,如果为空,则添加表示空数组的字符串"[]"
。然后,它遍历数组中的每个元素,如果元素是数组类型,它会根据数组的具体类型调用相应的toString
方法或递归调用deepToString
方法。如果元素是对象类型,它会直接调用toString
方法。如果检测到自引用,它会添加"[...]"
来表示。
这种递归处理方式确保了即使数组嵌套了多层,每一层的内容都会被递归地考虑在内,从而生成一个反映数组所有深层内容的字符串表示。
14. 初始化所有元素
使用提供的生成器函数来初始化数组的所有元素。这两个方法都是从Java 1.8版本开始引入的。
setAll
是一个同步方法,它按顺序设置数组的每个元素。
parallelSetAll
则使用并行流来设置数组的每个元素,这可能会加快初始化大数组的速度。
下附对象类型的源码:
public static <T> void setAll(T[] array, IntFunction<? extends T> generator) {
// 检查生成器函数是否为null,如果是,则抛出NullPointerException
Objects.requireNonNull(generator);
// 遍历数组的每个索引
for (int i = 0; i < array.length; i++)
// 使用生成器函数计算当前索引位置的元素值,并将其赋值给数组的当前位置
array[i] = generator.apply(i);
}
setAll
方法接受两个参数:一个泛型数组 array
和一个生成器函数 generator
。生成器函数是一个 IntFunction
接口的实例,它接受一个整数索引并返回一个泛型类型 T
的对象。方法遍历数组的每个位置,调用生成器函数来计算每个位置的值,并将这个值赋给数组对应的位置。
public static <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator) {
// 检查生成器函数是否为null,如果是,则抛出NullPointerException
Objects.requireNonNull(generator);
// 创建一个并行整数流,范围从0到数组长度
// 对于流中的每个索引,使用生成器函数计算值,并将其赋值给数组的相应位置
IntStream.range(0, array.length).parallel().forEach(i -> { array[i] = generator.apply(i); });
}
parallelSetAll
方法也接受同样的两个参数。不同之处在于,它使用了 IntStream.range(0, array.length).parallel()
来创建一个并行流,这个流将在多个线程上执行。对于流中的每个索引,它使用生成器函数来计算值,并将这个值赋给数组的相应位置。由于操作是并行执行的,对于大型数组,这可能会比顺序执行更快,但是也可能导致生成器函数被并发调用,因此生成器函数必须是线程安全的。
初始类型同理。
15. 分割数据
返回一个覆盖指定数组所有元素的Spliterator
对象。
Spliterator
是Java 8引入的一个用于遍历和分割数据源(如集合、数组等)的接口,它被用于并行迭代数据元素。
以下是该方法的源码和解释:
public static <T> Spliterator<T> spliterator(T[] array) {
// 使用Spliterators工具类的spliterator方法创建一个新的Spliterator
// 第一个参数是要遍历的数组
// 第二个参数是一个组合的特征集,指定了Spliterator的特性
return Spliterators.spliterator(array,
Spliterator.ORDERED | Spliterator.IMMUTABLE);
}
方法接受一个泛型数组array
作为参数,并返回一个Spliterator<T>
对象。这个Spliterator
对象是使用Spliterators.spliterator
工具方法创建的,该方法接受数组和一组特征作为参数。
这个Spliterator
具有以下特征:
Spliterator.ORDERED
:元素有确定的顺序(例如,数组元素的顺序)。Spliterator.IMMUTABLE
:元素的源(在这种情况下是数组)不能被修改。这意味着在使用Spliterator
时,假定数组不会被修改。
这些特征是通过位或操作(|
)组合在一起传递给Spliterators.spliterator
方法的。
Spliterator
对象可以用于顺序或并行处理数组元素。它的trySplit
方法可以用来分割任务,以便并行处理,而tryAdvance
方法可以用来顺序处理元素。这些方法提供了一种灵活的迭代数组元素的方式,尤其是在需要并行处理时。
该方法自Java 1.8版本以来就存在,是Java平台上集合框架的一部分。
同时,该方法也支持不同的类型和指定范围,指定范围的时候需要传入startInclusive
(包含在内的起始索引)和endExclusive
(排除在外的结束索引),其他同理的内容就不赘述了。
16. stream流
返回一个以指定数组为数据源的顺序(sequential)Stream
对象。
Stream
是Java 8中引入的一个新的抽象,它代表了一系列的元素支持顺序和并行聚合操作。
源码:
public static <T> Stream<T> stream(T[] array) {
// 调用重载的stream方法,传入数组以及要处理的起始索引和结束索引
// 这里传入的起始索引是0,结束索引是数组的长度,意味着整个数组都将作为数据源
return stream(array, 0, array.length);
}
public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive) {
// 使用StreamSupport类的stream方法创建一个新的Stream
// 第一个参数是一个Spliterator,它定义了要遍历的数组范围
// 第二个参数是一个布尔值,指定创建的Stream是否为并行Stream,这里传入false表示创建顺序Stream
return StreamSupport.stream(spliterator(array, startInclusive, endExclusive), false);
}
第一个方法简单地调用第二个方法,传入整个数组的范围(从0到array.length
)。第二个方法使用StreamSupport.stream
工厂方法和spliterator
方法来创建流。spliterator
方法创建一个Spliterator
对象,它是一个可分割迭代器,用于遍历数组的指定范围。StreamSupport.stream
方法接受这个Spliterator
和一个指示是否创建并行流的布尔值。在这里,传入false
表示创建的是一个顺序流。
如果startInclusive
是负数,endExclusive
小于startInclusive
,或者endExclusive
大于数组大小,方法将抛出ArrayIndexOutOfBoundsException
。
这两个方法自Java 1.8版本以来就存在,是Java平台上集合框架的一部分。使用Stream
可以轻松地进行各种操作,如映射(map)、过滤(filter)、排序(sorted)和聚合(reduce)等。由于返回的是顺序流,所以操作将按照数组元素的顺序执行。如果需要并行处理数组元素,可以调用Stream
的parallel()
方法将顺序流转换为并行流。
基本数据类型同理。
读完本文的朋友可能留下很多问题:
正零和负零有什么区别?
什么是泛型,如何应用?
什么是并行流,如何应用?
什么是线程安全?
Spliterator是什么,如何应用?
StreamSupport类是什么?
我们带着问题继续看。
————————————————————
本专栏是【小问题】系列,旨在每篇解决一个小问题,并秉持着刨根问底的态度解决这个问题可能带出的一系列问题。