JavaRush /Java 博客 /Random-ZH /喝咖啡休息#130。如何正确使用 Java 数组 - Oracle 的提示

喝咖啡休息#130。如何正确使用 Java 数组 - Oracle 的提示

已在 Random-ZH 群组中发布
来源:Oracle 使用数组可以包括反射、泛型和 lambda 表达式。 我最近与一位使用 C 进行开发的同事交谈。话题转向了数组以及它们在 Java 中与 C 相比如何工作。考虑到 Java 被认为是一种类似 C 的语言,我觉得这有点奇怪。他们其实有很多相似之处,但也有不同之处。让我们从简单开始吧。 喝咖啡休息#130。 如何正确使用 Java 数组 - Oracle 提示 - 1

数组声明

如果您遵循 Java 教程,您将看到有两种声明数组的方法。第一个很简单:
int[] array; // a Java array declaration
您可以看到它与 C 有何不同,其中语法为:
int array[]; // a C array declaration
让我们再次回到Java。声明数组后,您需要分配它:
array = new int[10]; // Java array allocation
是否可以同时声明和初始化数组?其实没有:
int[10] array; // NOPE, ERROR!
但是,如果您已经知道这些值,则可以立即声明并初始化数组:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
如果你不知道其含义怎么办?以下是您最常看到的用于声明、分配和使用int数组的代码:
int[] array;
array = new int[10];
array[0] = 0;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 3;
array[5] = 5;
array[6] = 8;
...
请注意,我指定了一个int数组,它是Java 基本数据类型的数组让我们看看如果您使用 Java 对象数组而不是基元尝试相同的过程会发生什么:
class SomeClass {
    int val;
    // …
}
SomeClass[] array = new SomeClass[10];
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
如果我们运行上面的代码,我们会在尝试使用数组的第一个元素后立即得到一个异常。为什么?即使数组已分配,数组的每个段都包含空对象引用。如果您将此代码输入到 IDE 中,它甚至会自动为您填充 .val,因此错误可能会令人困惑。要修复该错误,请按照下列步骤操作:
SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //new code
    array[i] = new SomeClass();             //new code
}                                           //new code
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
但这并不优雅。我想知道为什么我不能用更少的代码轻松分配数组和数组中的对象,甚至可能全部在一行上。为了找到答案,我进行了多次实验。

在 Java 数组中寻找必杀技

我们的目标是优雅地编写代码。遵循“干净代码”的规则,我决定创建可重用的代码来清理数组分配模式。这是第一次尝试:
public class MyArray {

    public static Object[] toArray(Class cls, int size)
      throws Exception {
        Constructor ctor = cls.getConstructors()[0];
        Object[] objects = new Object[size];
        for ( int i = 0; i < size; i++ ) {
            objects[i] = ctor.newInstance();
        }

        return objects;
    }

    public static void main(String[] args) throws Exception {
        SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32); // see this
        System.out.println(array1);
    }
}
多亏了toArray实现 ,标记为“see this”的代码行看起来正是我想要的方式。此方法使用反射来查找所提供的类的默认构造函数,然后调用该构造函数来实例化该类的对象。该过程为每个数组元素调用一次构造函数。极好!只是可惜,行不通。该代码可以正常编译,但在运行时会抛出ClassCastException错误。要使用此代码,您需要创建一个Object元素数组,然后将数组的每个元素转换为SomeClass类,如下所示:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
这不优雅!经过更多实验,我使用反射、泛型和 lambda 表达式开发了几种解决方案。

解决方案1:使用反射

这里我们使用java.lang.reflect.Array类来实例化您指定的类的数组,而不是使用基java.lang.Object类。这本质上是一行代码更改:
public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // new code
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // new code
    }
    return (Object[])array;
}
您可以使用这种方法来获取所需类的数组,然后像这样使用它:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
尽管这不是必需的更改,但第二行已更改为使用 Array 反射类来设置每个数组元素的内容。这真太了不起了!但还有一个细节似乎不太正确:对SomeClass[] 的转换看起来不太好。幸运的是,有一个泛型解决方案。

解决方案 2:使用泛型

集合框架使用泛型进行类型绑定,并在许多操作中消除了对泛型的强制转换。泛型也可以用在这里。我们以java.util.List为例。
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
除非您像这样更新第一行,否则上面代码片段中的第三行将引发错误:
List<SomeClass> = new ArrayList();
您可以通过在MyArray类 中使​​用泛型来实现相同的结果。这是新版本:
public class MyArray<E> {
    public <E> E[] toArray(Class cls, int size) throws Exception {
        E[] array = (E[])Array.newInstance(cls, size);
        Constructor ctor = cls.getConstructors()[0];
        for ( int element = 0; element < array.length; element++ ) {
            Array.set(array, element, ctor.newInstance());
        }
        return arrayOfGenericType;
    }
}
// ...
MyArray<SomeClass> a1 = new MyArray(SomeClass.class, 32);
SomeClass[] array1 = a1.toArray();
看上去不错。通过使用泛型并在声明中包含目标类型,可以在其他操作中推断出该类型。此外,通过执行以下操作,可以将该代码减少为一行:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
任务完成了,对吗?嗯,不完全是。如果您不关心调用哪个类构造函数,这很好,但如果您想调用特定的构造函数,那么此解决方案不起作用。可以继续使用反射来解决这个问题,但是这样代码就会变得复杂。幸运的是,有 lambda 表达式提供了另一种解决方案。

解决方案 3:使用 lambda 表达式

我承认,我以前对 lambda 表达式并不是特别感兴趣,但我已经学会欣赏它们。我特别喜欢java.util.stream.Stream接口,它处理对象集合。Stream帮助我达到了 Java 数组的必杀技。这是我第一次尝试使用 lambda:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
为了便于阅读,我将这段代码分成三行。您可以看到它满足了所有要求:它简单而优雅,创建了对象实例的填充数组,并允许您调用特定的构造函数。注意toArray方法参数:SomeClass[]::new。这是一个生成器函数,用于分配指定类型的数组。然而,就目前情况而言,这段代码有一个小问题:它创建了一个无限大小的数组。这不是很理想。但是这个问题可以通过调用limit方法来解决:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
该数组现在限制为 32 个元素。你甚至可以为数组的每个元素设置特定的对象值,如下所示:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
这段代码展示了 lambda 表达式的强大功能,但代码并不整洁或紧凑。在我看来,调用另一个构造函数来设置该值会好得多。
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
我喜欢基于 lambda 表达式的解决方案。当您需要调用特定的构造函数或处理数组的每个元素时,它是理想的选择。当我需要更简单的东西时,我通常使用基于泛型的解决方案,因为它更简单。然而,您可以亲眼看到 lambda 表达式提供了一种优雅且灵活的解决方案。

结论

今天我们学习了如何在 Java 中 声明和分配基元数组、分配Object元素数组、使用反射、泛型和 lambda 表达式。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION