重拾Java基础知识:数组

前言

简单来看,数组需要你去创建和初始化,你可以通过下标对数组元素进行访问,数组的大小不会改变。大多数时候你只需要知道这些,但有时候你必须在数组上进行更复杂的操作,你也可能需要在数组和更加灵活的 集合 (Collection)之间做出评估。因此本章我们将对数组进行更加深入的分析。

数组的特性

将数组和其他类型的集合区分开来的原因有三:效率,类型,保存基本数据类型的能力。在 Java 中,使用数组存储和随机访问对象引用序列是非常高效的。数组是简单的线性序列,这使得对元素的访问变得非常快。然而这种高速也是有代价的,代价就是数组对象的大小是固定的,且在该数组的生存期内不能更改。

速度通常并不是问题,如果有问题,你保存和检索对象的方式也很少是罪魁祸首。你应该总是从 ArrayList 开始,它将数组封装起来。必要时,它会自动分配更多的数组空间,创建新数组,并将旧数组中的引用移动到新数组。这种灵活性需要开销,所以一个 ArrayList 的效率不如数组。在极少的情况下效率会成为问题,所以这种时候你可以直接使用数组。

数组和集合(Collections)都不能滥用。不管你使用数组还是集合,如果你越界,你都会得到一个 RuntimeException 的异常提醒,这表明你的程序中存在错误。

在泛型前,其他的集合类以一种宽泛的方式处理对象(就好像它们没有特定类型一样)。事实上,这些集合类把保存对象的类型默认为 Object,也就是 Java 中所有类的基类。而数组是优于 预泛型 (pre-generic)集合类的,因为你创建一个数组就可以保存特定类型的数据。这意味着你获得了一个编译时的类型检查,而这可以防止你插入错误的数据类型,或者搞错你正在提取的数据类型。

当然,不管在编译时还是运行时,Java都会阻止你犯向对象发送不正确消息的错误。然而不管怎样,使用数组都不会有更大的风险。比较好的地方在于,如果编译器报错,最终的用户更容易理解抛出异常的含义。

一个数组可以保存基本数据类型,而一个预泛型的集合不可以。然而对于泛型而言,集合可以指定和检查他们保存对象的类型,而通过 自动装箱 (autoboxing)机制,集合表现地就像它们可以保存基本数据类型一样,因为这种转换是自动的。

显示数组的实用程序

Java 提供了 Arrays.toString() 来将数组转换为可读字符串,然后可以在控制台上显示。

public class ArraysShow {
    public static void main(String[] args) {
        String[] strings = new String[]{"1", "2", "3"};
        System.out.println(strings);
        System.out.println(Arrays.toString(strings));
        /** Output:
         *  [Ljava.lang.String;@29453f44
         *  [1, 2, 3]
         */
    }
}

一等对象

不管你使用的什么类型的数组,数组中的数据集实际上都是对堆中真正对象的引用。数组是保存指向其他对象的引用的对象,数组可以隐式地创建,作为数组初始化语法的一部分,也可以显式地创建,比如使用一个 new 表达式。数组对象的一部分(事实上,你唯一可以使用的方法)就是只读的 length 成员函数,它能告诉你数组对象中可以存储多少元素。[ ] 语法是你访问数组对象的唯一方式。

public class ArraysShow {
    public static void main(String[] args) {
        int[] ints = new int[5];
        String[] strings = {"a","b","c"};
        double[] doubles = new double[]{1.0,1.1,1.2};
        System.out.println(ints.length);
        System.out.println(strings.length);
        System.out.println(doubles.length);
        /** Output:
         *  5
         *  3
         *  3
         */
    }
}

返回数组

假设你写了一个方法,这个方法不是返回一个元素,而是返回多个元素。对 C++/C 这样的语言来说这是很困难的,因为你无法返回一个数组,只能是返回一个指向数组的指针。这会带来一些问题,因为对数组生存期的控制变得很混乱,这会导致内存泄露。

而在 Java 中,你只需返回数组,你永远不用为数组担心,只要你需要它,它就可用,垃圾收集器会在你用完后把它清理干净。

public class ArraysShow {
    public String[] respArray(){
        return new String[5];
    }
}

多维数组

要创建多维的基元数组,你要用大括号来界定数组中的向量:

public class ArraysShow {
    public static void main(String[] args) {
        int[][] ints = new int[2][3];
        System.out.println(Arrays.deepToString(ints));
        int[][] ints2 = {{1,2,3},{4,5,6}};
        System.out.println(Arrays.deepToString(ints2));
        /** Output:
         *  [[0, 0, 0], [0, 0, 0]]
         *  [[1, 2, 3], [4, 5, 6]]
         */
    }
}

你也可以使用 new 分配数组,倘若你不对基元数组进行显式的初始化,它的值会自动初始化。这个例子使用 Arrays.deepToString() 方法,将多维数组转换成 String 类型,就像输出中显示的那样。

泛型数组

一般来说,数组和泛型并不能很好的结合。你不能实例化参数化类型的数组,类型擦除需要删除参数类型信息,而且数组必须知道它们所保存的确切类型,以强制保证类型安全。

public class ArraysShow<T> {
    public T[] f(T[] t){
        return t;
    }
    public static void main(String[] args) {
        new ArraysShow<String>().f(new String[2]);
    }
}

比起使用参数化类,使用参数化方法很方便。您不必为应用它的每个不同类型都实例化一个带有参数的类,但是可以使它成为 静态 的。你不能总是选择使用参数化方法而不用参数化的类,但通常参数化方法是更好的选择。

Arrays工具类

方法说明
asList()获取任何序列或数组,并将其转换为一个 列表集合
copyOf()以新的长度创建现有数组的新副本。
copyOfRange()创建现有数组的一部分的新副本。
equals()比较两个数组是否相等。
deepEquals()多维数组的相等性比较。
stream()生成数组元素的流。
hashCode()生成数组的哈希值(您将在附录中了解这意味着什么:理解equals()和hashCode())。
deepHashCode()多维数组的哈希值。
sort()排序数组
parallelSort()对数组进行并行排序,以提高速度。
binarySearch()在已排序的数组中查找元素。
parallelPrefix()使用提供的函数并行累积(以获得速度)。基本上,就是数组的reduce()。
spliterator()从数组中产生一个Spliterator;这是本书没有涉及到的流的高级部分。
toString()为数组生成一个字符串表示。你在整个章节中经常看到这种用法。
deepToString()为多维数组生成一个字符串。你在整个章节中经常看到这种用法。对于所有基本类型和对象,所有这些方法都是重载的。

数组拷贝

copyOf() 方法复制指定长度的数组和 copyOfRange() 方法复制范围数组,与使用for循环手工执行复制相比要快得多。这些方法被重载以处理所有类型。如果你阅读过ArrayList的底层源码,你会发现它的扩容方式就是这样。

public class ArraysShow {
    public static void main(String[] args) {
        int[] ints = {1,2,3,4,5,6};
        int[] copyOf = Arrays.copyOf(ints, ints.length);
        System.out.println(Arrays.toString(copyOf));
        int[] copyOfRange = Arrays.copyOfRange(ints, 0, 3);
        System.out.println(Arrays.toString(copyOfRange));
        /** Output:
         *  [1, 2, 3, 4, 5, 6]
         *  [1, 2, 3]
         */
    }
}

数组比较

数组 提供了 equals() 来比较一维数组,以及 deepEquals() 来比较多维数组。对于所有原生类型和对象,这些方法都是重载的。

数组相等的含义:数组必须有相同数量的元素,并且每个元素必须与另一个数组中的对应元素相等,对每个元素使用 equals()对于原生类型,使用原生类型的包装类的 equals() 方法;

public class ArraysShow {
    public static void main(String[] args) {
        int[] ints = {1,2,3,4,5,6};
        int[] ints2 = {1,2,3,4,5,6,7};
        System.out.println(Arrays.equals(ints,ints2));
        int[][] int3 = {{1,2,3},{4,5,6}};
        int[][] int4 = {{1,2,3},{4,5,7}};
        System.out.println(Arrays.deepEquals(int3,int4));
        /** Output:
         *  false
         *  false
         */
    }
}

流和数组

stream() 方法很容易从某些类型的数组中生成元素流。

public class ArraysShow {
    public static void main(String[] args) {
        int[] ints = {1,2,3,4,5,6};
        int asInt = Arrays.stream(ints).max().getAsInt();
        System.out.println(asInt);
        Arrays.stream(ints).skip(3).forEach(System.out::print);
        System.out.println();
        Arrays.stream(ints).limit(3).forEach(System.out::print);
        /** Output:
         *  6
         *  456
         *  123
         */
    }
}

数组排序

使用内置的排序方法,您可以对实现了 Comparable 接口或具有 Comparator 的任何对象数组 或 任何原生数组进行排序。

public class ArraysShow {
    public static void main(String[] args) {
        String[] str = new String[]{"b","d","e","a","c"};
        Arrays.sort(str);
        System.out.println(Arrays.toString(str));
        Arrays.sort(str, Comparator.reverseOrder());
        System.out.println(Arrays.toString(str));
        /** Output:
         *  [a, b, c, d, e]
         *  [e, d, c, b, a]
         */
    }
}

注意字符串排序算法中的输出。它是字典式的,所以它把所有以大写字母开头的单词放在前面,然后是所有以小写字母开头的单词。

Java标准库中使用的排序算法被设计为最适合您正在排序的类型----原生类型的快速排序和对象的归并排序。

binarySearch二分查找

一旦数组被排序,您就可以通过使用 Arrays.binarySearch() 来执行对特定项的快速搜索。但是,如果尝试在未排序的数组上使用 binarySearch(),结果是不可预测的。

public class ArraysShow {
    public static void main(String[] args) {
        int[] ints = {655,123,51,311,548,151};
        int i = Arrays.binarySearch(ints, 51);
        System.out.println("index:"+i+",value:"+ints[i]);
        /** Output:
         *  index:2,value:51
         */
    }
}

parallelPrefix并行前缀

它对前一个元素和当前元素执行一个操作,并将结果放入当前元素位置:

public class ArraysShow {
    public static void main(String[] args) {
        int[] ints = {655,123,51,311,548,151};
        Arrays.parallelPrefix(ints, Integer::sum);
        System.out.println(Arrays.toString(ints));
        /** Output:
         *  [655, 778, 829, 1140, 1688, 1839]
         */
    }
}

在位置0中,它将先前计算的值(因为没有先前的值)与原始数组位置0中的值组合在一起。在位置1中,它获取之前计算的值(它只是存储在位置0中),并将其与位置1中先前计算的值相结合。依次往复。

本章小结

Java为固定大小的低级数组提供了合理的支持。在Java的后续版本中,集合支持得到了显著的改进,现在集合在除性能外的所有方面都优于数组,即使这样,集合的性能也得到了显著的改进。

使用自动装箱和泛型,在集合中保存原生类型是毫不费力的,这进一步鼓励您用集合替换低级数组。由于泛型产生类型安全的集合,数组在这方面也不再有优势。所有这些问题都表明,在使用Java的最新版本进行编程时,应该“优先选择集合而不是数组”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值