静态数据初始化
无论创建了多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此他只作用于域。如果一个域时基本的静态类型域,且也没有对他进行初始化,那么它就会获得基本类型的标准初值:如果它是一个对象引用,那么它的默认初始化值就是null。
想要了解静态存储区域是何时初始化的,看下面这个例子:
class Bowl{
Bowl(int marker){
System.out.println("Bowl( "+marker+" )");
}
void f1(int marker){
System.out.println("f1( "+marker+" )");
}
}
class Table{
static Bowl bowl1=new Bowl(1);
Table(){
System.out.println("table()");
bowl2.f1(1);
}
void f2(int marker){
System.out.println("f2( "+marker+" )");
}
static Bowl bowl2=new Bowl(2);
}
class Cupboard{
Bowl bowl3=new Bowl(3);
static Bowl bowl4=new Bowl(4);
Cupboard(){
System.out.println("cupboard");
bowl4.f1(2);
}
void f3(int marker){
System.out.println("f3( "+marker+" )");
}
static Bowl bowl5=new Bowl(5);
}
/**
* Staticinitialization
*/
public class Staticinitialization {
public static void main(String[] args) {
System.out.println("creating new cupboard() in main");
new Cupboard();
System.out.println("creating new cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table=new Table();
static Cupboard cupboard=new Cupboard();
}
结果
Bowl( 1 )
Bowl( 2 )
table()
f1( 1 )
Bowl( 4 )
Bowl( 5 )
Bowl( 3 )
cupboard
f1( 2 )
creating new cupboard() in main
Bowl( 3 )
cupboard
f1( 2 )
creating new cupboard() in main
Bowl( 3 )
cupboard
f1( 2 )
f2( 1 )
f3( 1 )
Bowl类使得看到类的创建,而Table类和Cupboard类再它们的类定义中加入了Bowl类型的静态数据成员。注意,在静态数据成员定义之前,Cupboard类先定义了一个Bowl类型的非静态数据成员b3
由输出可见,静态初始化只要在必要时刻才会进行。如果不创建Table对象,也不引用Table.b1或Table.b2,那么静态的Bowl b1和b2永远不会被创建。只有在第一个Table对象被创建的时候,它们才会被初始化,此后,静态对象不会再次被初始化。
初始化的顺序是先静态对象,而后是非静态对象。
总结一下对象的创建过程,假设有个名为Dog的类
- 即使没有显式的使用static关键词,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时,或者Dog类的静态方法被访问时,Java解释器必须查找类路径,定位Dog.class文件
- 然后载入Dog.class,有关静态初始化的所有动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。
- 当new Dog()创建对象的时候,首先将在Dog对象分配足够的存储空间。
- 这块存储空间会被清零,这就自动地将Dog对象中的所有基本类型数据都设置成了默认值,而引用则被设置成了null
- 执行所有出现字段定义处的初始化动作
- 执行构造器。
数组
编译器不允许指定数组的大小。这就又把我们带回有关“引用”的问题上。现在拥有的只是对数组的一个引用,而且也没给数组对象分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方。但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况写,存储空间的分配(相当于new)将由编译器负责。例如
int [] a1{1,2,3,4,5,6};
那么,为什么还要在没有数组的时候定义一个数组引用呢?
int[] a2;
在Java总,可以将一个数组赋值给另一个数组,所以可以这样
a2=a1
其实真正做的只是复制了一个引用,就像下面
public class Arr {
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("a["+i+"] = "+a1[i]);
}
}
}
输出结果
a[0] = 2
a[1] = 3
a[2] = 4
a[3] = 5
a[4] = 6
可以看到代码中给出a1的初始值,但a2却没有;在本例子中,a2是在后面被赋给另一个数组的。由于a2和a1都是相同数组的别名,因此通过a2所做的修改在a1中可以看到。
所以数组都有一个固定成员,可以通过它获知数组内包含了多少个元素,但不能对其修改,这个成员就是length。
如果在编写程序时,并不能确定数组里需要多少个元素,该怎么办,可以直接用new在数组里面创建元素。尽管创建的是基本类型数组,new仍然可以工作。
import java.util.Arrays;
import java.util.Random;
/**
* Arr
*/
public class Arr {
public static void main(String[] args) {
int[] a;
Random rand=new Random();
a=new int[rand.nextInt(20)];
System.out.println("length of a = "+a.length);
System.out.println(Arrays.toString(a));
}
}
数组的大小通过Random.nextInt()方法随机决定的,这个方法会返回0到输入参数之间的一个值。这表面数组的创建确实是在运行时刻进行的。此外,程序输出表面:数组元素中的基本数据类型值会自动初始化为空值(对于数字和字符,就是0;对于布尔型,是false)
Arrays.toString()方法属于Java.util标准类库,它将产生一维数组的可打印版本。
数组下标范围越界,编译器不会报错,但会抛出异常
本例中,数组也可以在定义的同时进行初始化:
int[] a=new int[rand.nextInt(20)];
如果你创建一个非基本类型的数组,那么你就创建了一个引用数组。以整型的包装器类Integer为例,它是一个类而不是基本类型
import java.util.Arrays;
import java.util.Random;
public class Arr {
public static void main(String[] args) {
Random rand =new Random(47);
Integer[] a=new Integer[rand.nextInt(20)];
System.out.println(a.length);
for(int i=0;i<a.length;i++){
a[i]=rand.nextInt(100);
}
System.out.println(Arrays.toString(a));
}
}
这里,即便使用new创建数组之后
Integer[] a=new Integer[rand.nextInt(20)];
它还只是一个引用数组,并且直到通过创建新的Integer对象,并把对象赋值给引用,初始化进程才算结束:
a[i]=rand.nextInt(100);
可以创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用来替换传递给该main()方法的命令行参数。
public class Arr {
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.println(s+" ");
}
}
}
fiddle
de
dum
可变参数列表
上面那种形式提供了一种方便的语法来创建对象并调用方法。由于所有的类都是直接或间接继承于object类,所以可以创建Object数组为参数的方法。
class A{}
public class VarArgs {
static void printArray(Object[] args){
for(Object obj : args){
System.out.println(obj+" ");
}
System.out.println("");
}
public static void main(String[] args) {
printArray(new Object[]{new Integer(47),new Float(3.14),new Double(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
也可以这样写(新版本)
public class VarArgs {
static void printArray(Object... args){
for(Object obj:args){
System.out.println(obj+" ");
}
System.out.println("");
}
public static void main(String[] args) {
printArray(47,3.14f,11.11);
printArray("one","two","three");
}
}
有了可变参数,就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你取填充数组。你获取的仍旧是一个数组。因此,如果你由一组事物,可以把它们当作列表传递,而且如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。
enum
Java程序员再需要使用枚举类型时,必须了解很多细节并需要格外仔细,以正确地产生enum的效果。
public enum Spiciness{
NOT,
MILD,
MEDIUM,
HOT,
FLAMING
}
为了使用enum,需要创建一个该类型的引用,并将其赋值给某个实例
public enum Spiciness{
NOT,
MILD,
MEDIUM,
HOT,
FLAMING
}
public class EnumOrder {
public static void main(String[] args) {
Spiciness howHot=Spiciness.MEDIUM;
System.out.println(howHot);
}
}
在你创建enum时,编译器会自动添加一些有用的特性。例如,他会创建toString()方法,以便你可以很方便地显示某个enum实例的名字,编译器还会创建ordinal()方法,用来表示某个特定enum常量的声明顺序,已经static values()方法,用来按照enum常量的声明顺序。
enum Spicy{
NOT,
MILD,
MEDIUM,
HOT
}
public class enum2 {
public static void main(String[] args) {
for(Spicy s:Spicy.values()){
System.out.println(s+" ordinal "+s.ordinal());
}
}
}
NOT ordinal 0
MILD ordinal 1
MEDIUM ordinal 2
HOT ordinal 3