不该被忽视的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类很多内容都是从虚拟机底层实现的。
package java.lang.reflect; /**
* The <code>Array</code> class provides static methods to dynamically create and
* access Java arrays.
*
* <p><code>Array</code> permits widening conversions to occur during a get or set
* operation, but throws an <code>IllegalArgumentException</code> if a narrowing
* conversion would occur.
*
* @author Nakul Saraiya
*/
public final
class Array { private Array() {} public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
} public static Object newInstance(Class<?> componentType, int... dimensions)
throws IllegalArgumentException, NegativeArraySizeException {
return multiNewArray(componentType, dimensions);
}
// 说明length字段是在虚拟机底层维护的
public static native int getLength(Object array)
throws IllegalArgumentException; public static native Object get(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native boolean getBoolean(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native byte getByte(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native char getChar(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native short getShort(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native int getInt(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native long getLong(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native float getFloat(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native double getDouble(Object array, int index)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void set(Object array, int index, Object value)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setBoolean(Object array, int index, boolean z)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setByte(Object array, int index, byte b)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setChar(Object array, int index, char c)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setShort(Object array, int index, short s)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setInt(Object array, int index, int i)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setLong(Object array, int index, long l)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setFloat(Object array, int index, float f)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; public static native void setDouble(Object array, int index, double d)
throws IllegalArgumentException, ArrayIndexOutOfBoundsException; private static native Object newArray(Class componentType, int length)
throws NegativeArraySizeException; // length传入虚拟机底层,由本地方法处理,侧面说明length字段是JVM底层维护的 private static native Object multiNewArray(Class componentType,
int[] dimensions)
throws IllegalArgumentException, NegativeArraySizeException;
}
粗略地阅读源代码,看到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); //
System.out.println(Array.getLength(a)); //
不该被忽视的CoreJava细节(四)的更多相关文章
- 不该被忽视的CoreJava细节(一)
一.系列文章导言 <不该被忽视的CoreJava细节>系列文章将会持续更新.我希望自己通过这一系列文章的写作,能与读者一起进步,逐步完善对Java体系结构的了解. 二.本期关注点 几乎翻看 ...
- 不该被忽视的CoreJava细节(三)
一.不该被遗忘的移位位运算 本文主要介绍移位运算(Shift Operation), 适当介绍一下其它相关的位运算. 甭说计算机刚发明那会,就连21世纪初那段日子,计算机内存都是KB/MB计算的.编写 ...
- ASP.NET常被忽视的一些细节
原文:ASP.NET常被忽视的一些细节 前段时间碰到一个问题:为什么在ASP.NET程序中定时器有时候会不工作? 这个问题看起来很奇怪,代码好像也没错,但就是结果与预期不一致. 其实这里是ASP.NE ...
- C语言里面关于数组的一个容易忽视的小细节
ginobili@VM_44_28_sles10sp1:~/code> cat test3.cpp #include <stdio.h> int main(){ char a[5] ...
- 谈谈一些有趣的CSS题目(四)-- 从倒影说起,谈谈 CSS 继承 inherit
开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...
- 谈谈一些有趣的CSS题目(十四)-- 纯 CSS 方式实现 CSS 动画的暂停与播放!
开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...
- Servlet的学习(四)
在本篇的Servlet的学习中,主要来学习由使用MyEclipse来开发Servlet的一些小细节. 细节一:在web.xml中可以对同一个Servlet配置多个对外访问路径,并如果在web.xml中 ...
- 【原创】构建高性能ASP.NET站点之三 细节决定成败
原文:[原创]构建高性能ASP.NET站点之三 细节决定成败 构建高性能ASP.NET站点之三 细节决定成败 前言:曾经就因为一个小小的疏忽,从而导致了服务器崩溃了,后来才发现:原来就是因为一个循环而 ...
- Gentoo 后的几个细节的完善
Gentoo 后的几个细节的完善 目录 Gentoo 后的几个细节的完善 细节一:引导分区与 cdrom 开机正确挂载 细节二:可预见的命名规则的网络接口名称改为传统的 eth0 细节三:为管理员用户 ...
随机推荐
- Lucene.net 搜索引擎的中文资料
以下是我找到的网上一些关于Lucene.net 搜索引擎的介绍资料 https://code.i-harness.com/zh-CN/tagged/lucene?page=5 http://jingp ...
- Linux操作系统的内存使用方法详细解析
我是一名程序员,那么我在这里以一个程序员的角度来讲解Linux内存的使用. 一提到内存管理,我们头脑中闪出的两个概念,就是虚拟内存,与物理内存.这两个概念主要来自于linux内核的支持. Linux在 ...
- HTML5秘籍(第2版) 中文pdf扫描版
HTML5秘籍(第2版)共包括四个部分,共13章.第一部分介绍了HTML5的发展历程,用语义元素构造网页,编写更有意义的标记,以及构建更好的Web表单.第二部分介绍了HTML5中的音频与视频.CS ...
- C#回调函数的简单讲解与应用例子
using System; namespace CallBackTest{ class Program //用户层,执行输入等操作 { static void Main(string[] args) ...
- Syslog+Fluentd+InfluxDB日志收集系统搭建
环境配置 节点 配置 类型 操作系统 Sched 2G 2CPU 50GB ens3=>192.168.200.11 KVM虚拟机 CentOS 7 Nova 4G 2CPU 50GB ens3 ...
- [Win10] 安装虚拟光驱 用于加载ISO等镜像文件
百度上找到UltraISO安装 一般来说安装到这就基本会显示一个 若经过上述步骤仍没出现虚拟光驱,则尝试进行加载ISO镜像文件到虚拟光驱然后再看看 这样基本就大功告成了~
- 微信JSApi支付---常见问题
1.支付一直报 “get_brand_wcpay_request:false” 错误 原因: 商户平台上设置的[支付授权目录]路劲不正确,比如:支付的页面的域名是:www.xxx.com/pay/s ...
- 如何在MySQL中设置外键约束
引用:http://blog.sina.com.cn/s/blog_53729e4601011wja.html MySql外键设置详解 (1) 外键的使用: 外键的作用,主要有两个: 一个是 ...
- 萌新在线模板--keyboarder_zsq
好像马上就要出去打铁了QAQ,所以是不是要做个模板带过去也玩一玩? 那就做吧... 标题就设为萌新模板吧...各种萌新讲解对吧.... 图论 拓扑排序 最短路 最小生成树 二分匹配 强连通Tarjan ...
- 谢特——后缀数组+tire 树
题目 [题目描述] 由于你成功地在 $ \text{1 s} $ 内算出了上一题的答案,英雄们很高兴并邀请你加入了他们的游戏.然而进入游戏之后你才发现,英雄们打的游戏和你想象的并不一样…… 英雄们打的 ...