六、初始化和清理(4)

news/2024/10/18 5:51:48/

本章概要

  • 数组的初始化
    • 动态数组创建
    • 可变参数列表
  • 枚举类型

数组初始化

数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符 [] 来定义和使用的。要定义一个数组引用,只需要在类型名加上方括号:

int[] a1;

方括号也可放在标识符的后面,两者的含义是一样的:

int a1[];

这种格式符合 C 和 C++ 程序员的习惯。不过前一种格式或许更合理,毕竟它表明类型是"一个 int 型数组"。本书中采用这种格式。

编译器不允许指定数组的大小。这又把我们带回有关"引用"的问题上。你所拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),但是还没有给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但是也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成。这种情况下,存储空间的分配(相当于使用 new) 将由编译器负责。例如:

int[] a1 = {1, 2, 3, 4, 5};

那么为什么在还没有数组的时候定义一个数组引用呢?

int[] a2;

在 Java 中可以将一个数组赋值给另一个数组,所以可以这样:

a2 = a1;

其实真正做的只是复制了一个引用,就像下面演示的这样:

// housekeeping/ArraysOfPrimitives.javapublic class ArraysOfPrimitives {public static void main(String[] args) {int[] a1 = {1, 2, 3, 4, 5};int[] a2;a2 = a1;for (int i = 0; i < a2.length; i++) {a2[i] += 1;}for (int i = 0; i < a1.length; i++) {System.out.println("a1[" + i + "] = " + a1[i]);}}
}

输出:

a1[0] = 2;
a1[1] = 3;
a1[2] = 4;
a1[3] = 5;
a1[4] = 6;

a1 初始化了,但是 a2 没有;这里,a2 在后面被赋给另一个数组。由于 a1a2 是相同数组的别名,因此通过 a2 所做的修改在 a1 中也能看到。

所有的数组(无论是对象数组还是基本类型数组)都有一个固定成员 length,告诉你这个数组有多少个元素,你不能对其修改。与 C 和 C++ 类似,Java 数组计数也是从 0 开始的,所能使用的最大下标数是 length - 1。超过这个边界,C 和 C++ 会默认接受,允许你访问所有内存,许多声名狼藉的 bug 都是由此而生。但是 Java 在你访问超出这个边界时,会报运行时错误(异常),从而避免此类问题。

动态数组创建

如果在编写程序时,不确定数组中需要多少个元素,可以使用 new 在数组中创建元素。如下例所示,使用 new 创建基本类型数组。new 不能创建非数组以外的基本类型数据:

// housekeeping/ArrayNew.java
// Creating arrays with new
import java.util.*;public class ArrayNew {public static void main(String[] args) {int[] a;Random rand = new Random(47);a = new int[rand.nextInt(20)];System.out.println("length of a = " + a.length);System.out.println(Arrays.toString(a));} 
}

输出:

length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

数组的大小是通过 Random.nextInt() 随机确定的,这个方法会返回 0 到输入参数之间的一个值。 由于随机性,很明显数组的创建确实是在运行时进行的。此外,程序输出表明,数组元素中的基本数据类型值会自动初始化为默认值(对于数字和字符是 0;对于布尔型是 false)。Arrays.toString()java.util 标准类库中的方法,会产生一维数组的可打印版本。

本例中,数组也可以在定义的同时进行初始化:

int[] a = new int[rand.nextInt(20)];

如果可能的话,应该尽量这么做。

如果你创建了一个非基本类型的数组,那么你创建的是一个引用数组。以整型的包装类型 Integer 为例,它是一个类而非基本类型:

// housekeeping/ArrayClassObj.java
// Creating an array of nonprimitive objectsimport java.util.*;public class ArrayClassObj {public static void main(String[] args) {Random rand = new Random(47);Integer[] a = new Integer[rand.nextInt(20)];System.out.println("length of a = " + a.length);for (int i = 0; i < a.length; i++) {a[i] = rand.nextInt(500); // Autoboxing}System.out.println(Arrays.toString(a));}
}

输出:

length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]

这里,即使使用 new 创建数组之后:

Integer[] a = new Integer[rand.nextInt(20)];

它只是一个引用数组,直到通过创建新的 Integer 对象(通过自动装箱),并把对象赋值给引用,初始化才算结束:

a[i] = rand.nextInt(500);

如果忘记了创建对象,但试图使用数组中的空引用,就会在运行时产生异常。

也可以用花括号括起来的列表来初始化数组,有两种形式:

// housekeeping/ArrayInit.java
// Array initialization
import java.util.*;public class ArrayInit {public static void main(String[] args) {Integer[] a = {1, 2,3, // Autoboxing};Integer[] b = new Integer[] {1, 2,3, // Autoboxing};System.out.println(Arrays.toString(a));System.out.println(Arrays.toString(b));}
}

输出:

[1, 2, 3]
[1, 2, 3]

在这两种形式中,初始化列表的最后一个逗号是可选的(这一特性使维护长列表变得更容易)。

尽管第一种形式很有用,但是它更加受限,因为它只能用于数组定义处。第二种形式可以用在任何地方,甚至用在方法的内部。例如,你创建了一个 String 数组,将其传递给另一个类的 main() 方法,如下:

// housekeeping/DynamicArray.java
// Array initializationpublic class DynamicArray {public static void main(String[] args) {Other.main(new String[] {"fiddle", "de", "dum"});}
}class Other {public static void main(String[] args) {for (String s: args) {System.out.print(s + " ");}}
}

输出:

fiddle de dum

Other.main() 的参数是在调用处创建的,因此你甚至可以在方法调用处提供可替换的参数。

可变参数列表

你可以以一种类似 C 语言中的可变参数列表(C 通常把它称为"varargs")来创建和调用方法。这可以应用在参数个数或类型未知的场合。由于所有的类都最后继承于 Object 类,所以你可以创建一个以 Object 数组为参数的方法,并像下面这样调用:

// housekeeping/VarArgs.java
// Using array syntax to create variable argument listsclass A {}public class VarArgs {static void printArray(Object[] args) {for (Object obj: args) {System.out.print(obj + " ");}System.out.println();}public static void main(String[] args) {printArray(new Object[] {47, (float) 3.14, 11.11});printArray(new Object[] {"one", "two", "three"});printArray(new Object[] {new A(), new A(), new A()});}
}

输出:

47 3.14 11.11 
one two three 
A@15db9742 A@6d06d69c A@7852e922

printArray() 的参数是 Object 数组,使用 for-in 语法遍历和打印数组的每一项。标准 Java 库能输出有意义的内容,但这里创建的是类的对象,打印出的内容是类名,后面跟着一个 @ 符号以及多个十六进制数字。因而,默认行为(如果没有定义 toString() 方法的话,后面会讲这个方法)就是打印类名和对象的地址。

你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来,就像在 printArray() 中看到的那样:

// housekeeping/NewVarArgs.java
// Using array syntax to create variable argument listspublic class NewVarArgs {static void printArray(Object... args) {for (Object obj: args) {System.out.print(obj + " ");}System.out.println();}public static void main(String[] args) {// Can take individual elements:printArray(47, (float) 3.14, 11.11);printArray(47, 3.14F, 11.11);printArray("one", "two", "three");printArray(new A(), new A(), new A());// Or an array:printArray((Object[]) new Integer[] {1, 2, 3, 4});printArray(); // Empty list is OK}
}

输出:

47 3.14 11.11 
47 3.14 11.11 
one two three 
A@15db9742 A@6d06d69c A@7852e922 
1 2 3 4

有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组,这就是为什么 printArray() 可以使用 for-in 迭代数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换。注意程序的倒数第二行,一个 Integer 数组(通过自动装箱创建)被转型为一个 Object 数组(为了移除编译器的警告),并且传递给了 printArray()。显然,编译器会发现这是一个数组,不会执行转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法会把它们当作可变参数列表来接受。

程序的最后一行表明,可变参数的个数可以为 0。当具有可选的尾随参数时,这一特性会有帮助:

// housekeeping/OptionalTrailingArguments.javapublic class OptionalTrailingArguments {static void f(int required, String... trailing) {System.out.print("required: " + required + " ");for (String s: trailing) {System.out.print(s + " ");}System.out.println();}public static void main(String[] args) {f(1, "one");f(2, "two", "three");f(0);}
}

输出:

required: 1 one 
required: 2 two three 
required: 0

这段程序展示了如何使用除了 Object 类之外类型的可变参数列表。这里,所有的可变参数都是 String 对象。可变参数列表中可以使用任何类型的参数,包括基本类型。下面例子展示了可变参数列表变为数组的情形,并且如果列表中没有任何元素,那么转变为大小为 0 的数组:

// housekeeping/VarargType.javapublic class VarargType {static void f(Character... args) {System.out.print(args.getClass());System.out.println(" length " + args.length);}static void g(int... args) {System.out.print(args.getClass());System.out.println(" length " + args.length)}public static void main(String[] args) {f('a');f();g(1);g();System.out.println("int[]: "+ new int[0].getClass());}
}

输出:

class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I

getClass() 方法属于 Object 类,将在"类型信息"一章中全面介绍。它会产生对象的类,并在打印该类时,看到表示该类类型的编码字符串。前导的 [ 代表这是一个后面紧随的类型的数组,I 表示基本类型 int;为了进行双重检查,在最后一行创建了一个 int 数组,打印了其类型。这样也验证了使用可变参数列表不依赖于自动装箱,而使用的是基本类型。

然而,可变参数列表与自动装箱可以和谐共处,如下:

// housekeeping/AutoboxingVarargs.javapublic class AutoboxingVarargs {public static void f(Integer... args) {for (Integer i: args) {System.out.print(i + " ");}System.out.println();}public static void main(String[] args) {f(1, 2);f(4, 5, 6, 7, 8, 9);f(10, 11, 12);}
}

输出:

1 2
4 5 6 7 8 9
10 11 12

你可以在单个参数列表中将类型混合在一起,自动装箱机制会有选择地把 int 类型的参数提升为 Integer

可变参数列表使得方法重载更加复杂了,尽管乍看之下似乎足够安全:

// housekeeping/OverloadingVarargs.javapublic class OverloadingVarargs {static void f(Character... args) {System.out.print("first");for (Character c: args) {System.out.print(" " + c);}System.out.println();}static void f(Integer... args) {System.out.print("second");for (Integer i: args) {System.out.print(" " + i);}System.out.println();}static void f(Long... args) {System.out.println("third");}public static void main(String[] args) {f('a', 'b', 'c');f(1);f(2, 1);f(0);f(0L);//- f(); // Won's compile -- ambiguous}
}

输出:

first a b c
second 1
second 2 1
second 0
third

在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。

但是如果调用不含参数的 f(),编译器就无法知道应该调用哪个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员感到意外。

你可能会通过在某个方法中增加一个非可变参数解决这个问题:

// housekeeping/OverloadingVarargs2.java
// {WillNotCompile}public class OverloadingVarargs2 {static void f(float i, Character... args) {System.out.println("first");}static void f(Character... args) {System.out.println("second");}public static void main(String[] args) {f(1, 'a');f('a', 'b');}
}

{WillNotCompile} 注释把该文件排除在了本书的 Gradle 构建之外。如果你手动编译它,会得到下面的错误信息:

OverloadingVarargs2.java:14:error:reference to f is ambiguous f('a', 'b');
\^
both method f(float, Character...) in OverloadingVarargs2 and method f(Character...) in OverloadingVarargs2 match 1 error

如果你给这两个方法都添加一个非可变参数,就可以解决问题了:

// housekeeping/OverloadingVarargs3public class OverloadingVarargs3 {static void f(float i, Character... args) {System.out.println("first");}static void f(char c, Character... args) {System.out.println("second");}public static void main(String[] args) {f(1, 'a');f('a', 'b');}
}

输出:

first
second

你应该总是在重载方法的一个版本上使用可变参数列表,或者压根不用它。

枚举类型

Java 5 中添加了一个看似很小的特性 enum 关键字,它使得我们在需要群组并使用枚举类型集时,可以很方便地处理。以前,你需要创建一个整数常量集,但是这些值并不会将自身限制在这个常量集的范围内,因此使用它们更有风险,而且更难使用。枚举类型属于非常普遍的需求,C、C++ 和其他许多语言都已经拥有它了。在 Java 5 之前,Java 程序员必须了解许多细节并格外仔细地去达成 enum 的效果。现在 Java 也有了 enum,并且它的功能比 C/C++ 中的完备得多。下面是个简单的例子:

// housekeeping/Spiciness.javapublic enum Spiciness {NOT, MILD, MEDIUM, HOT, FLAMING
}

这里创建了一个名为 Spiciness 的枚举类型,它有5个值。由于枚举类型的实例是常量,因此按照命名惯例,它们都用大写字母表示(如果名称中含有多个单词,使用下划线分隔)。

要使用 enum,需要创建一个该类型的引用,然后将其赋值给某个实例:

// housekeeping/SimpleEnumUse.javapublic class SimpleEnumUse {public static void main(String[] args) {Spiciness howHot = Spiciness.MEDIUM;System.out.println(howHot);}
}

输出:

MEDIUM

在你创建 enum 时,编译器会自动添加一些有用的特性。例如,它会创建 toString() 方法,以便你方便地显示某个 enum 实例的名称,这从上面例子中的输出可以看出。编译器还会创建 ordinal() 方法表示某个特定 enum 常量的声明顺序,static values() 方法按照 enum 常量的声明顺序,生成这些常量值构成的数组:

// housekeeping/EnumOrder.javapublic class EnumOrder {public static void main(String[] args) {for (Spiciness s: Spiciness.values()) {System.out.println(s + ", ordinal " + s.ordinal());}}
}

输出:

NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4

尽管 enum 看起来像是一种新的数据类型,但是这个关键字只是在生成 enum 的类时,产生了某些编译器行为,因此在很大程度上你可以将 enum 当作其他任何类。事实上,enum 确实是类,并且具有自己的方法。

enum 有一个很实用的特性,就是在 switch 语句中使用:

// housekeeping/Burrito.javapublic class Burrito {Spiciness degree;public Burrito(Spiciness degree) {this.degree = degree;}public void describe() {System.out.print("This burrito is ");switch(degree) {case NOT:System.out.println("not spicy at all.");break;case MILD:case MEDIUM:System.out.println("a little hot.");break;case HOT:case FLAMING:default:System.out.println("maybe too hot");}}public static void main(String[] args) {Burrito plain = new Burrito(Spiciness.NOT),greenChile = new Burrito(Spiciness.MEDIUM),jalapeno = new Burrito(Spiciness.HOT);plain.describe();greenChile.describe();jalapeno.describe();}
}

输出:

This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.

由于 switch 是在有限的可能值集合中选择,因此它与 enum 是绝佳的组合。注意,enum 的名称是如何能够倍加清楚地表明程序的目的的。

通常,你可以将 enum 用作另一种创建数据类型的方式,然后使用所得到的类型。这正是关键所在,所以你不用过多地考虑它们。在 enum 被引入之前,你必须花费大量的精力去创建一个等同的枚举类型,并是安全可用的。

这些介绍对于你理解和使用基本的 enum 已经足够了,我们会在"枚举"一章中进行更深入的探讨。


http://www.ppmy.cn/news/998202.html

相关文章

Typescript 第八章 异步编程,并行和并发(JavaScript事件循环,异步流,多线程类型安全)

Typescript第八章 异步编程&#xff0c;并发和并行 异步API&#xff0c;比如说回调&#xff0c;promise和流。 JavaScript引擎在一个线路中多路复用任务&#xff0c;而其他任务则处于空闲状态。这种事件循环是JavaScript引擎的标准线程模型。 多路复用是指在一个线程中同时处…

ThreadLocal原理

ThreadLocal原理 ThreadLocal对象new出来存放到堆中&#xff0c;ThreadLocal引用是存放在栈里 Thread 类有个 ThreadLocalMap 成员变量&#xff0c;Map的key是Threadlocal 对象&#xff0c;value是你要存放的线程局部变量。 public void set(T value) {//获取当前线程Thread&…

python elasticsearch update by query

创建索引以及添加数据 PUT test {"mappings": {"properties": {"test":{"type": "nested"}} }}GET test/_mappingPUT test/_doc/1 {"test":{"name":"ellis","age":100} }elastics…

为机器人装“大脑” 谷歌发布RT-2大模型

大语言模型不仅能让应用变得更智能&#xff0c;还将让机器人学会举一反三。在谷歌发布RT-1大模型仅半年后&#xff0c;专用于机器人的RT-2大模型于近期面世&#xff0c;它能让机器人学习互联网上的文本和图像&#xff0c;并具备逻辑推理能力。 该模型为机器人智能带来显著升级…

【C++】基于多设计模式下的同步异步日志系统

✍作者&#xff1a;阿润021 &#x1f4d6;专栏&#xff1a;C 文章目录 一、项目介绍二、项目实现准备工作1.日志系统技术实现策略2.相关技术知识补充2.1 不定参函数设计2.2 设计模式 三、日志项目框架设计1.模块划分2.各模块关系图 四、详细代码实现1.实用工具类设计2.日志等级…

SOP/详解*和**/python数据结构(iter,list,tuple,dict)/ 解包

一、错误解决合集 1. > combined_seq.named_children() 2. isinstance 2th parameter : must be a type or tuple of types > 改为tuple&#xff0c;不要用列表。改为 LLLayer (nn.Conv2d,nn.Linear) 3. File “test.py”, line 90, in calculate_fin_fout print(“hi”…

机器学习(一)---概述

文章目录 1.人工智能、机器学习、深度学习2.机器学习的工作流程2.1 获取数据集2.2 数据基本处理2.3 特征工程2.3.1 特征提取2.3.2 特征预处理2.3.3 特征降维 2.4 机器学习2.5 模型评估 3.机器学习的算法分类3.1 监督学习3.1.1 回归问题3.1.2 分类问题 3.2 无监督学习 1.人工智能…

低代码开发平台源码为个性化应用开发提速,助推企业数字化转型升级

低代码开发是一种通过可视化编程工具和预构建组件来快速构建应用程序的方法。它的核心理念是通过最大程度地减少手写代码的数量&#xff0c;让开发者能够专注于业务逻辑&#xff0c;而非繁琐的编码工作。在当今数字化时代&#xff0c;快速、高效地开发应用程序对企业的成功至关…