不该被忽视的CoreJava细节(四)

令人纳闷的数组初始化细节

这个细节问题我很久以前就想深入研究一下,但是一直没有能够抽出时间,借这系列文章的东风,尽量解决掉这个"心头病"。

下面以一维int数组为例,对数组初始化方式进行分类。

1) int[] a = new int[2]; a[0] = 1; a[1] = 2;

2) int[] a = new int[]{1, 2};

3) int[] a; a = new int[]{1, 2};

4) int[] a = {1, 2};

这四种初始化方式都是合理的。

但有的时候,题目中会来这么一手。

int[] a;
a = {1, 2};  // 编译错误,不符合语法规则

虽然我们能够通过编译器测试其是否正确,但是你不觉得纳闷么?这难道是一个特例?

于是乎,对Java的数组的理解变得更加模糊。本文将深入探究Java数组的机制,解决这个1年内没时间思考的问题。

事实胜于雄辩

曾经我认为直接赋值数组是属于Java常量池技术范畴。通过一段对数组的测试代码,将会证明这是错误观点。

public class ArrayDemo {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        int[] b = {1, 2, 3};
        System.out.println(a.hashCode()); // 1072840587
        System.out.println(b.hashCode()); // 959045497
        System.out.println(a == b);       // false
    }
}

明确一点:对象hashCode值一样,并不一定能说明这两个引用指向同一个对象;反过来,如果hashCode值不一样,则引用指向的一定是不同的对象。

因此,引用变量a、b指向的是不同的对象。也就是说,Java并没有为直接赋值数组提供运行时常量池技术实现。



在eclipse中写int数组,先声明,后赋值形式,会报错:"数组常量仅能在初始化时使用",如下所示。

为什么在C语言中很容易理解的概念,在Java中却变得如此晦涩难懂?

在C语言中,这个问题的解释很容易。c[]代表一个int数组,其中数组名c代表数组首地址,是一个常量,不能作为左值。

但是在Java语言规范(可以参考相应文献)中,明确将数组定义为一种引用数据类型,数组名是一个变量,储存数组对象的句柄值。但是,我们知道Java语言是一门高级编程语言,大致属于第三代编程语言(第一代汇编,第二代C,二代半C++,第三代Java,第四代R等),其底层实现依靠C++。建立在C++基础之上的Java很多底层细节都被屏蔽,导致使用者很难一窥究竟,这就是为什么我们可以理解C/C++的基本概念,但是对Java却一头雾水的原因。

这个例子中,似乎数组与字符串等别的对象确实有些不同。



当时想到,既然Collection(接口)、Collections(工具类),那么会不会有Array(接口)、Arrays(工具类)组合形式,毕竟都是一个没有s,一个有s。用eclipse查看源代码的时候,发现真的有Array这个类,只不过Array不是猜想的接口,而是一个普通类。

查看Array类的源代码,发现其中的方法都是用native修饰的,意味着Array类很多内容都是从虚拟机底层实现的。

 1 package java.lang.reflect;
 2
 3 /**
 4  * The <code>Array</code> class provides static methods to dynamically create and
 5  * access Java arrays.
 6  *
 7  * <p><code>Array</code> permits widening conversions to occur during a get or set
 8  * operation, but throws an <code>IllegalArgumentException</code> if a narrowing
 9  * conversion would occur.
10  *
11  * @author Nakul Saraiya
12  */
13 public final
14 class Array {
15
16     private Array() {}
17
18     public static Object newInstance(Class<?> componentType, int length)
19     throws NegativeArraySizeException {
20     return newArray(componentType, length);
21     }
22
23     public static Object newInstance(Class<?> componentType, int... dimensions)
24     throws IllegalArgumentException, NegativeArraySizeException {
25     return multiNewArray(componentType, dimensions);
26     }
27   // 说明length字段是在虚拟机底层维护的
28     public static native int getLength(Object array)
29     throws IllegalArgumentException;
30
31     public static native Object get(Object array, int index)
32      throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
33
34     public static native boolean getBoolean(Object array, int index)
35     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
36
37     public static native byte getByte(Object array, int index)
38     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
39
40     public static native char getChar(Object array, int index)
41     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
42
43     public static native short getShort(Object array, int index)
44     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
45
46     public static native int getInt(Object array, int index)
47     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
48
49     public static native long getLong(Object array, int index)
50     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
51
52     public static native float getFloat(Object array, int index)
53     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
54
55     public static native double getDouble(Object array, int index)
56     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
57
58     public static native void set(Object array, int index, Object value)
59     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
60
61     public static native void setBoolean(Object array, int index, boolean z)
62     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
63
64     public static native void setByte(Object array, int index, byte b)
65     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
66
67     public static native void setChar(Object array, int index, char c)
68     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
69
70     public static native void setShort(Object array, int index, short s)
71     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
72
73     public static native void setInt(Object array, int index, int i)
74     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
75
76     public static native void setLong(Object array, int index, long l)
77     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
78
79     public static native void setFloat(Object array, int index, float f)
80     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
81
82     public static native void setDouble(Object array, int index, double d)
83     throws IllegalArgumentException, ArrayIndexOutOfBoundsException;
84
85     private static native Object newArray(Class componentType, int length)
86     throws NegativeArraySizeException;  // length传入虚拟机底层,由本地方法处理,侧面说明length字段是JVM底层维护的
87
88     private static native Object multiNewArray(Class componentType,
89     int[] dimensions)
90     throws IllegalArgumentException, NegativeArraySizeException;
91 }

粗略地阅读源代码,看到public static native int getLength();方法,其中有native关键字,而且在Array类中并没有length字段。那么,可以判定Array类的length字段一定是JVM在运行时维护的。

不入虎穴,焉得虎子

接下来介绍一些比较细节的问题。如果我们打印数组引用变量中储存的值,会发现一个有趣的东西。

public class ArrayTest {
    public static void main(String[] args) {
        Object[] o = new Object[2];
        System.out.println(o);   //[Ljava.lang.Object;@6154283a

        String[] str = new String[2];
        System.out.println(str); //[Ljava.lang.String;@5c1d29c1

        Throwable[] t = new Throwable[2];
        System.out.println(t);   //[Ljava.lang.Throwable;@7ea06d25

        @SuppressWarnings("rawtypes")
        Class[] c = new Class[2];
        System.out.println(c);  //[Ljava.lang.Class;@565dd915
   -----------------------------------------以上均属于 Object[]类型------------------------------------------    

-----------------------------------------以下均属于  基本类型数组------------------------------------------
        byte[] b = new byte[2];
        System.out.println(b);  //[B@2b571dff

        boolean[] bl = new boolean[2];
        System.out.println(bl);  //[Z@64726693

        short[] s = new short[2];
        System.out.println(s);  //[S@12ac706a

        int[] i = new int[2];
        System.out.println(i);  //[I@770848b9

        long[] l = new long[2];
        System.out.println(l);  //[J@40dea6bc

        char[] ch = new char[2];
        System.out.println(ch); //两个方框
        System.out.println(ch.getClass().getName());   //[C
        char[] ch = new char[2];
        ch[0] = ‘a‘;
        ch[1] = ‘b‘;
        System.out.println(ch);  //ab

        float[] f = new float[2];
        System.out.println(f);  //[F@5994a1e9

        double[] d = new double[2];
        System.out.println(d);  //[D@2d11f5f1
    }
}            

除了char[]比较特殊,主要是因为String内部构造是由char[]实现的,所以直接打印char[]对象引用的值时会出现输出的是值和别的数组类型不一样。

为什么数组对象句柄是这种以"[字母@散列码"格式?查看JDK中的jni.h头文件(C++)时,终于明白了其中的缘由。

   

经过分析,可以得出这样的结论:

以上九种数组类型,数组引用变量存的内容都是符合:[字母@散列码";

以上五种非数组类型。Object类型、Class类型、Throwable类型、String类型、Array类型,打印符合Java规范的常见格式:类限定名@散列码。

还有一个,曾经我在找数组的length字段到底从哪儿来的?

分析的情况无非两种:1)从父类继承来的; 2)自己拥有的。

遗憾的是,并不能从任何地方找到有length属性,这是判断倾向于自己拥有的。可是,好像又有什么地方不对劲,自己拥有的,在哪里?

看过jni.h源代码后,我才发现,原来直接赋值数组的length字段是虚拟机运行时维护的。

真相到底是什么

绕了个大弯,我们还没有解决掉文章最初的那个问题。为什么直接赋值数组不能以如下方式初始化。

实际上,Java对数组的处理表面上是当成类似引用数据类型,在本质上(虚拟机底层)还是采用C++那套模型。

int[] a;
a = {1, 2};   // 编译器认为 a 是一个常量,采用C++模型
int[] a = {1, 2};   // 编译器认为 a 是一个引用变量,采用C++模型

也就是说,Java直接赋值数组其实和C++直接赋值数组在本质上没有什么两样,都认为数组变量是常量。只不过,在Java语言为了面向对象的协调性而将数组看成特殊的引用数据类型而已。关于将Java将数组看成特殊的引用数据类型这一点,可以从引用数据类型的分类(数组、类、接口)中看出,数组作为一个特例被Java纳到对象的范畴。

后记

本来想尝试从字节码角度来分析的,这得花更多时间去研究,想想还是算了。以后有机会或许会从字节码角度再来看这个问题吧。

想补充一个知识点,以前我们获取数组的长度一般使用"ref.length"。现在多了一种方法"Array.getLength(ref)",举例如下。

int[] a = {1, 2, 3, 4};
System.out.println(a.length);    // 4
System.out.println(Array.getLength(a));   // 4
时间: 2024-10-11 16:06:49

不该被忽视的CoreJava细节(四)的相关文章

被忽视的赚钱细节与渠道

当你冥想自己遨游世界,驾驶着玛莎拉蒂,驰骋在美丽的海滨公路,美女如云的世界,到处都是甜蜜的笑脸,最美丽的女人都想给你生小孩,那感觉多么的爽啊,被人尊敬被人景仰,可以呼风唤雨,无所不能,所到之处可以掀起一股正能量,每个人都会因为我的幸福而幸福,太爽了,在充满爱的国度里,我们尽情的发挥我们的灵感,我们的智能越来越高,我们的寿命越 来越长,我们成为了无所不能的神,我们就像暗黑破坏神里的正义力量,我们探索世界,快速推荐人类社会发展,这就是未来的我,你们一定会爱我爱得不能自己,来吧 被忽视的赚钱细节与渠道

ASP.NET常被忽视的一些细节

原文:ASP.NET常被忽视的一些细节 前段时间碰到一个问题:为什么在ASP.NET程序中定时器有时候会不工作? 这个问题看起来很奇怪,代码好像也没错,但就是结果与预期不一致. 其实这里是ASP.NET应用程序中一个容易被忽略的经节. 后来想想,类似这样的细节问题何止这一个,我今天就把我能想到的容易被忽视的细节问题都写出来,希望大家小心这些问题. 想到我以前的博客中也零散的说过了一些,所以这篇博客中也把它们列出来了, 不过,对于以前谈过的内容,这里将只会简略地说明. HttpContext.Cu

C#容易被忽视的知识点(四)

19.扩展类 例如想给某个类添加一个新方法,但由于某些原因,不能直接修改类的源代码,可以通过扩展类的方法实现. 具体例子如下: 假设有个类Show 1 public class Show 2 { 3 public void MethodOne() 4 { 5 Console.WriteLine("MethodOne"); 6 } 7 } 8 9 //扩展类 10 public static class ShowExtension 11 { 12 public static void M

关于子类继承父类的一个容易忽视的小细节

子类继承父类的时候对父类的属性有没有继承呢??(其实我自己刚学,一些细节的不是太清楚) 下面的代码会说明一个容易忽视的细节: 这是父类: public class FatherClass { int a = 10; public void fun(){ System.out.println("This is FatherClass funMothed!!"); }} 这是子类: public class Child extends FatherClass { int a = 20; p

C语言里面关于数组的一个容易忽视的小细节

[email protected]_44_28_sles10sp1:~/code> cat test3.cpp #include <stdio.h> int main(){ char a[5] = {0}; char *pa = a; printf("a = %p, pa = %p, &a=%p, &pa=%p\n", a, pa, &a, &pa); return 0;}[email protected]_44_28_sles10sp

Java面试题总结(一)

折磨人的小妖精---main方法 真的有公司会这么变态,用main方法来折磨你吗?你得承认,的确有比较low的公司会这么做.本文对这些与main方法相关的问题做一个小结. 1.下列程序运行结果是 public class Hello { public static void main(String s) { System.out.println("Hello"); } } A.编译错误 B.运行输出 "Hello" C.编译无错,但运行时指示没有定义构造方法 D.编

谈谈一些有趣的CSS题目(四)-- 从倒影说起,谈谈 CSS 继承 inherit

开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉到生僻的 CSS 属性,赶紧去补习一下吧. 不断更新,不断更新,不断更新,重要的事情说三遍. 所有题目汇总在我的 Github . 4.从倒影说起,谈谈 CSS 继承 inherit 给定一张有如下背景图的 div: 制作如下的倒影效果: 方法很多,但是我们当然要寻找最快最便捷的方法,至少得是无论图

个性婚礼蛋糕,10个细节问题

个性婚礼蛋糕,10个细节问题 婚礼蛋糕从款式到口味,各种各样,新人们在挑选的时候,面对琳琅满目的蛋糕,常常感到无从下手,今天,猪八喜婚博网小编为大家整理了10个挑选个性婚礼蛋糕的黄金法则,希望能够为大家提供帮助,帮你挑选到一款能够成功吸引全场宾客目光的婚礼蛋糕,成为婚礼的另一主角. 细节一:蛋糕与婚礼的主题想匹配 定制婚礼蛋糕首选要考虑婚礼场地的大小和来宾人数,这两项是决定蛋糕体量的关键因素.其次是蛋糕预算,想呈现的婚礼主题.风格.口味,还包括烤纸的颜色.造型及其他附加装饰,最好准备参考图片事先

Servlet的学习(四)

在本篇的Servlet的学习中,主要来学习由使用MyEclipse来开发Servlet的一些小细节. 细节一:在web.xml中可以对同一个Servlet配置多个对外访问路径,并如果在web.xml中配置的信息服务器会自动加载部署,而如果是在Servlet中进行程序代码的修改,则每次都要重新部署. 首先,在使用MyEclipse创建Servlet后,会根据所创建的Servlet进行到web.xml文件的映射,如下图所示: 经过这个映射之后,在web.xml文件中就自动生成了这个Servlet的配