计算平均值
计算一组数字(在本例中为整数)的平均值可以通过两个简单的步骤实现:
- 计算数组中元素的和。
- 将此总和除以数组的长度。
在代码行中,我们有以下内容:
public static double average(int[] arr) { return sum(arr) / arr.length; } public static double sum(int[] arr) { double sum = 0; for (int elem: arr) { sum += elem; } return sum; }
整数数组的平均值为 2.0:
double avg = MathArrays.average(integers);
在 Java8 函数式风格中,此问题的解决方案需要一行代码:
double avg = Arrays.stream(integers).average().getAsDouble();
对于第三方库支持,请考虑 Apache Common Lang(ArrayUtil
)和 Guava 的Chars
、Ints
、Longs
以及其他类。
105 反转数组
这个问题有几种解决办法。它们中的一些改变了初始数组,而另一些只是返回一个新数组
假设以下整数数组:
int[] integers = {-1, 2, 3, 1, 4, 5, 3, 2, 22};
让我们从一个简单的实现开始,它将数组的第一个元素与最后一个元素交换,第二个元素与倒数第二个元素交换,依此类推:
public static void reverse(int[] arr) { for (int leftHead = 0, rightHead = arr.length - 1; leftHead < rightHead; leftHead++, rightHead--) { int elem = arr[leftHead]; arr[leftHead] = arr[rightHead]; arr[rightHead] = elem; } }
前面的解决方案改变了给定的数组,这并不总是期望的行为。当然,我们可以修改它以返回一个新的数组,也可以依赖 Java8 函数样式,如下所示:
// 22, 2, 3, 5, 4, 1, 3, 2, -1 int[] reversed = IntStream.rangeClosed(1, integers.length) .map(i -> integers[integers.length - i]).toArray();
现在,让我们反转一个对象数组。为此,让我们考虑一下Melon
类:
public class Melon { private final String type; private final int weight; // constructor, getters, equals(), hashCode() omitted for brevity }
另外,让我们考虑一个Melon
数组:
Melon[] melons = { new Melon("Crenshaw", 2000), new Melon("Gac", 1200), new Melon("Bitter", 2200) };
第一种解决方案需要使用泛型来塑造实现,该实现将数组的第一个元素与最后一个元素交换,将第二个元素与最后一个元素交换,依此类推:
public static <T> void reverse(T[] arr) { for (int leftHead = 0, rightHead = arr.length - 1; leftHead < rightHead; leftHead++, rightHead--) { T elem = arr[leftHead]; arr[leftHead] = arr[rightHead]; arr[rightHead] = elem; } }
因为我们的数组包含对象,所以我们也可以依赖于Collections.reverse()。我们只需要通过Arrays.asList()
方法将数组转换成List
:
// Bitter(2200g), Gac(1200g), Crenshaw(2000g) Collections.reverse(Arrays.asList(melons));
前面的两个解决方案改变了数组的元素。Java8 函数式风格可以帮助我们避免这种变异:
// Bitter(2200g), Gac(1200g), Crenshaw(2000g) Melon[] reversed = IntStream.rangeClosed(1, melons.length) .mapToObj(i -> melons[melons.length - i]) .toArray(Melon[]:new);
对于第三方库支持,请考虑 Apache Common Lang(ArrayUtils.reverse()和 Guava 的Lists类。
106 填充和设置数组
有时,我们需要用一个固定值填充数组。例如,我们可能希望用值1填充整数数组。实现这一点的最简单方法依赖于一个for语句,如下所示:
int[] arr = new int[10]; // 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 for (int i = 0; i < arr.length; i++) { arr[i] = 1; }
但我们可以通过Arrays.fill()
方法将此代码简化为一行代码。对于基本体和对象,此方法有不同的风格。前面的代码可以通过Arrays.fill(int[] a, int val)
重写如下:
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 Arrays.fill(arr, 1);
Arrays.fill() also come with flavors for filling up just a segment/range of an array. For integers, this method is fill(int[] a, int fromIndexInclusive, int toIndexExclusive, int val).
现在,应用一个生成函数来计算数组的每个元素怎么样?例如,假设我们要将每个元素计算为前一个元素加 1。最简单的方法将再次依赖于for语句,如下所示:
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 for (int i = 1; i < arr.length; i++) { arr[i] = arr[i - 1] + 1; }
根据需要应用于每个元素的计算,必须相应地修改前面的代码。
对于这样的任务,JDK8 附带了一系列的Arrays.setAll()和Arrays.parallelSetAll()方法。例如,前面的代码片段可以通过setAll(int[] array, IntUnaryOperator generator)重写如下:
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 Arrays.setAll(arr, t -> { if (t == 0) { return arr[t]; } else { return arr[t - 1] + 1; } });
除此之外,我们还有setAll(double[] array, IntToDoubleFunction generator)、setAll(long[] array, IntToLongFunction generator)和setAll(T[] array, IntFunction<? extends T> generator)。
根据生成器的功能,此任务可以并行完成,也可以不并行完成。例如,前面的生成器函数不能并行应用,因为每个元素都依赖于前面元素的值。尝试并行应用此生成器函数将导致不正确和不稳定的结果。
但是假设我们要取前面的数组(1,2,3,4,5,6,7,8,9,10),然后将每个偶数值乘以它本身,将每个奇数值减去 1。因为每个元素都可以单独计算,所以在这种情况下我们可以授权一个并行进程。这是Arrays.parallelSetAll()方法的完美工作。基本上,这些方法是用来并行化Arrays.setAll()方法的。
现在我们将parallelSetAll(int[] array, IntUnaryOperator generator)应用于这个数组:
// 0, 4, 2, 16, 4, 36, 6, 64, 8, 100 Arrays.parallelSetAll(arr, t -> { if (arr[t] % 2 == 0) { return arr[t] * arr[t]; } else { return arr[t] - 1; } });
对于每个Arrays.setAll()方法,都有一个Arrays.parallelSetAll()方法。
作为奖励,Arrays附带了一组名为parallelPrefix()的方法。这些方法对于将数学函数应用于数组的元素(累积和并发)非常有用。
例如,如果我们要将数组中的每个元素计算为前面元素的和,那么我们可以如下所示:
// 0, 4, 6, 22, 26, 62, 68, 132, 140, 240 Arrays.parallelPrefix(arr, (t, q) -> t + q);
107 下一个更大的元素
NGE 是一个涉及数组的经典问题。
基本上,有一个数组和它的一个元素e
,我们要获取下一个(右侧)大于e
的元素。例如,假设以下数组:
int[] integers = {1, 2, 3, 4, 12, 2, 1, 4};
获取每个元素的 NGE 将产生以下对(-1 被解释为右侧的元素不大于当前元素):
1 : 2 2 : 3 3 : 4 4 : 12 12 : -1 2 : 4 1 : 4 4 : -1
这个问题的一个简单解决方案是循环每个元素的数组,直到找到一个更大的元素或者没有更多的元素要检查。如果我们只想在屏幕上打印对,那么我们可以编写一个简单的代码,如下所示:
public static void println(int[] arr) { int nge; int n = arr.length; for (int i = 0; i < n; i++) { nge = -1; for (int j = i + 1; j < n; j++) { if (arr[i] < arr[j]) { nge = arr[j]; break; } } System.out.println(arr[i] + " : " + nge); } }
另一个解决方案依赖于栈。主要是,我们在栈中推送元素,直到当前处理的元素大于栈中的顶部元素。当这种情况发生时,我们弹出那个元素。本书附带的代码中提供了解决方案。
108 更改数组大小
增加数组的大小并不简单。这是因为 Java 数组的大小是固定的,我们不能修改它们的大小。这个问题的解决方案需要创建一个具有所需大小的新数组,并将所有值从原始数组复制到这个数组中。这可以通过Arrays.copyOf()方法或System.arraycopy()(由Arrays.copyOf()内部使用)完成。
对于一个原始数组(例如,int),我们可以将数组的大小增加 1 后将值添加到数组中,如下所示:
public static int[] add(int[] arr, int item) { int[] newArr = Arrays.copyOf(arr, arr.length + 1); newArr[newArr.length - 1] = item; return newArr; }
或者,我们可以删除最后一个值,如下所示:
public static int[] remove(int[] arr) { int[] newArr = Arrays.copyOf(arr, arr.length - 1); return newArr; }
或者,我们可以按如下所示调整给定长度数组的大小:
public static int[] resize(int[] arr, int length) { int[] newArr = Arrays.copyOf(arr, arr.length + length); return newArr; }
捆绑到本书中的代码还包含了System.arraycopy()
备选方案。此外,它还包含泛型数组的实现。签名如下:
public static <T> T[] addObject(T[] arr, T item); public static <T> T[] removeObject(T[] arr); public static <T> T[] resize(T[] arr, int length);
在有利的背景下,让我们将一个相关的主题引入讨论:如何在 Java 中创建泛型数组。以下操作无效:
T[] arr = new T[arr_size]; // causes generic array creation error
有几种方法,但 Java 在copyOf(T[] original, int newLength)
中使用以下代码:
// newType is original.getClass() T[] copy = ((Object) newType == (Object) Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);
109 创建不可修改/不可变的集合
在 Java 中创建不可修改/不可变的集合可以很容易地通过Collections.unmodifiableFoo()方法(例如,unmodifiableList())完成,并且从 JDK9 开始,通过来自List、Set、Map和其他接口的一组of()方法完成。
此外,我们将在一组示例中使用这些方法来获得不可修改/不可变的集合。主要目标是确定每个定义的集合是不可修改的还是不可变的。
在阅读本节之前,建议先阅读第 2 章、“对象、不变性和switch表达式”中有关不变性的问题。
好吧。对于原始类型来说,这非常简单。例如,我们可以创建一个不可变的整数List,如下所示:
private static final List<Integer> LIST = Collections.unmodifiableList(Arrays.asList(1, 2, 3, 4, 5)); private static final List<Integer> LIST = List.of(1, 2, 3, 4, 5);
对于下一个示例,让我们考虑以下可变类:
public class MutableMelon { private String type; private int weight; // constructor omitted for brevity public void setType(String type) { this.type = type; } public void setWeight(int weight) { this.weight = weight; } // getters, equals() and hashCode() omitted for brevity }
问题 1 (Collections.unmodifiableList()
)
让我们通过Collections.unmodifiableList()
方法创建MutableMelon
列表:
// Crenshaw(2000g), Gac(1200g) private final MutableMelon melon1 = new MutableMelon("Crenshaw", 2000); private final MutableMelon melon2 = new MutableMelon("Gac", 1200); private final List<MutableMelon> list = Collections.unmodifiableList(Arrays.asList(melon1, melon2));
melon1.setWeight(0); melon2.setWeight(0);
现在,列表将显示以下西瓜(因此列表发生了变异):
Crenshaw(0g), Gac(0g)
问题 2 (Arrays.asList()
)
我们直接在Arrays.asList()
中硬编码实例,创建MutableMelon
列表:
private final List<MutableMelon> list = Collections.unmodifiableList(Arrays.asList( new MutableMelon("Crenshaw", 2000), new MutableMelon("Gac", 1200)));
那么,这个列表是不可修改的还是不变的?答案是不可更改的。当增变器方法抛出UnsupportedOperationException
时,硬编码实例可以通过List.get()
方法访问。一旦可以访问它们,它们就可以变异:
MutableMelon melon1 = list.get(0); MutableMelon melon2 = list.get(1); melon1.setWeight(0); melon2.setWeight(0);
现在,列表将显示以下西瓜(因此列表发生了变异):
Crenshaw(0g), Gac(0g)
问题 3 (Collections.unmodifiableList()
和静态块)
让我们通过Collections.unmodifiableList()
方法和static
块创建MutableMelon
列表:
private static final List<MutableMelon> list; static { final MutableMelon melon1 = new MutableMelon("Crenshaw", 2000); final MutableMelon melon2 = new MutableMelon("Gac", 1200); list = Collections.unmodifiableList(Arrays.asList(melon1, melon2)); }
那么,这个列表是不可修改的还是不变的?答案是不可更改的。虽然增变器方法会抛出UnsupportedOperationException
,但是硬编码的实例仍然可以通过List.get()
方法访问。一旦可以访问它们,它们就可以变异:
MutableMelon melon1l = list.get(0); MutableMelon melon2l = list.get(1); melon1l.setWeight(0); melon2l.setWeight(0);
现在,列表将显示以下西瓜(因此列表发生了变异):
Crenshaw(0g), Gac(0g)
问题 4 (List.of()
)
让我们通过List.of()
创建MutableMelon
的列表:
private final MutableMelon melon1 = new MutableMelon("Crenshaw", 2000); private final MutableMelon melon2 = new MutableMelon("Gac", 1200); private final List<MutableMelon> list = List.of(melon1, melon2);
那么,这个列表是不可修改的还是不变的?答案是不可更改的。虽然增变器方法会抛出UnsupportedOperationException
,但是硬编码的实例仍然可以通过List.get()
方法访问。一旦可以访问它们,它们就可以变异:
MutableMelon melon1l = list.get(0); MutableMelon melon2l = list.get(1); melon1l.setWeight(0); melon2l.setWeight(0);
现在,列表将显示以下西瓜(因此列表发生了变异):
Crenshaw(0g), Gac(0g)
对于下一个示例,让我们考虑以下不可变类:
public final class ImmutableMelon { private final String type; private final int weight; // constructor, getters, equals() and hashCode() omitted for brevity }
问题 5(不可变)
现在我们通过Collections.unmodifiableList()
和List.of()
方法创建ImmutableMelon
列表:
private static final ImmutableMelon MELON_1 = new ImmutableMelon("Crenshaw", 2000); private static final ImmutableMelon MELON_2 = new ImmutableMelon("Gac", 1200); private static final List<ImmutableMelon> LIST = Collections.unmodifiableList(Arrays.asList(MELON_1, MELON_2)); private static final List<ImmutableMelon> LIST = List.of(MELON_1, MELON_2);
那么,这个列表是不可修改的还是不变的?答案是不变的。增变器方法会抛出UnsupportedOperationException,我们不能对ImmutableMelon的实例进行变异。
根据经验,如果集合是通过unmodifiableFoo()或of()方法定义的,并且包含可变数据,则集合是不可修改的;如果集合是不可修改的,并且包含可变数据(包括原始类型),则集合是不可修改的。
需要注意的是,不可穿透的不变性应该考虑 Java 反射 API 和类似的 API,它们在操作代码时具有辅助功能。
对于第三方库支持,请考虑 Apache Common Collection、UnmodifiableList(和同伴)和 Guava 的ImmutableList(和同伴)。
在Map的情况下,我们可以通过unmodifiableMap()或Map.of()方法创建一个不可修改/不可修改的Map。
但我们也可以通过Collections.emptyMap()创建一个不可变的空Map:
Map<Integer, MutableMelon> emptyMap = Collections.emptyMap();
与emptyMap()类似,我们有Collections.emptyList()和Collections.emptySet()。在返回一个Map、List或Set的方法中,这些方法作为返回非常方便,我们希望避免返回null。
或者,我们可以通过Collections.singletonMap(K key, V value)用单个元素创建一个不可修改/不可变的Map:
// unmodifiable Map<Integer, MutableMelon> mapOfSingleMelon = Collections.singletonMap(1, new MutableMelon("Gac", 1200)); // immutable Map<Integer, ImmutableMelon> mapOfSingleMelon = Collections.singletonMap(1, new ImmutableMelon("Gac", 1200));
类似于singletonMap()
,我们有singletonList()
和singleton()
。后者用于Set
。
此外,从 JDK9 开始,我们可以通过一个名为ofEntries()
的方法创建一个不可修改的Map
。此方法以Map.Entry
为参数,如下例所示:
// unmodifiable Map.Entry containing the given key and value import static java.util.Map.entry; ... Map<Integer, MutableMelon> mapOfMelon = Map.ofEntries( entry(1, new MutableMelon("Apollo", 3000)), entry(2, new MutableMelon("Jade Dew", 3500)), entry(3, new MutableMelon("Cantaloupe", 1500)) );
或者,不可变的Map
是另一种选择:
Map<Integer, ImmutableMelon> mapOfMelon = Map.ofEntries( entry(1, new ImmutableMelon("Apollo", 3000)), entry(2, new ImmutableMelon("Jade Dew", 3500)), entry(3, new ImmutableMelon("Cantaloupe", 1500)) );
另外,可以通过 JDK10 从可修改/可变的Map
中获得不可修改/不可变的Map
,Map.copyOf(Map<? extends K,? extends V> map)
方法:
Map<Integer, ImmutableMelon> mapOfMelon = new HashMap<>(); mapOfMelon.put(1, new ImmutableMelon("Apollo", 3000)); mapOfMelon.put(2, new ImmutableMelon("Jade Dew", 3500)); mapOfMelon.put(3, new ImmutableMelon("Cantaloupe", 1500)); Map<Integer, ImmutableMelon> immutableMapOfMelon = Map.copyOf(mapOfMelon);
作为这一节的奖励,让我们来讨论一个不可变数组。
问题:我能用 Java 创建一个不可变数组吗?
答案:不可以。或者。。。有一种方法可以在 Java 中生成不可变数组:
static final String[] immutable = new String[0];
因此,Java 中所有有用的数组都是可变的。但是我们可以在Arrays.copyOf()
的基础上创建一个辅助类来创建不可变数组,它复制元素并创建一个新数组(在幕后,这个方法依赖于System.arraycopy()
。
因此,我们的辅助类如下所示:
import java.util.Arrays; public final class ImmutableArray<T> { private final T[] array; private ImmutableArray(T[] a) { array = Arrays.copyOf(a, a.length); } public static <T> ImmutableArray<T> from(T[] a) { return new ImmutableArray<>(a); } public T get(int index) { return array[index]; } // equals(), hashCode() and toString() omitted for brevity }
用法示例如下:
ImmutableArray<String> sample = ImmutableArray.from(new String[] { "a", "b", "c" });
110 映射的默认值
在 JDK8 之前,这个问题的解决方案依赖于辅助方法,它基本上检查Map中给定键的存在,并返回相应的值或默认值。这种方法可以在工具类中编写,也可以通过扩展Map接口来编写。通过返回默认值,我们可以避免在Map中找不到给定键时返回null。此外,这是依赖默认设置或配置的方便方法。
从 JDK8 开始,这个问题的解决方案包括简单地调用Map.getOrDefault()
方法。此方法获取两个参数,分别表示要在Map
方法中查找的键和默认值。当找不到给定的键时,默认值充当应该返回的备份值。
例如,假设下面的Map
封装了多个数据库及其默认的host:port
:
Map<String, String> map = new HashMap<>(); map.put("postgresql", "127.0.0.1:5432"); map.put("mysql", "192.168.0.50:3306"); map.put("cassandra", "192.168.1.5:9042");
我们来看看这个Map
是否也包含 Derby DB 的默认host:port
:
map.get("derby"); // null
由于映射中没有 Derby DB,因此结果将是null
。这不是我们想要的。实际上,当搜索到的数据库不在映射上时,我们可以在69:89.31.226:27017
上使用 MongoDB,它总是可用的。现在,我们可以很容易地将此行为塑造为:
// 69:89.31.226:27017 String hp1 = map.getOrDefault("derby", "69:89.31.226:27017"); // 192.168.0.50:3306 String hp2 = map.getOrDefault("mysql", "69:89.31.226:27017");
这种方法可以方便地建立流利的表达式,避免中断代码进行null
检查。请注意,返回默认值并不意味着该值将被添加到Map
。Map
保持不变。
111 计算映射中是否不存在/存在
有时,Map并不包含我们需要的准确的开箱即用条目。此外,当条目不存在时,返回默认条目也不是一个选项。基本上,有些情况下我们需要计算我们的入口。
对于这种情况,JDK8 提供了一系列方法:compute()、computeIfAbsent()、computeIfPresent()和merge()。在这些方法之间进行选择是一个非常了解每种方法的问题。
现在让我们通过示例来看看这些方法的实现。